Chapter1-data access reloaded:Entity Framework(上)

本章包括以下几个部分:

1.DataSet and classic ADO.NET approach
2.Object model approach
3.Object/relational mismatch
4.Entity Framework as a solution

 

当我们设计一个应用程序的时候,必须要决定如何访问并展现数据,这个决定几乎是最重要 的,你要考虑到性能,易于开发和维护等方面的问题,尽管有人尝试引入面向对象的数据库,但是在我们曾经写过的项目中,关系数据库是持久化数据的策略,而且在未来的很长时间里依然是。

时至今日,关系数据库提供了你持久化和获取数据的所有特征。你可以用表取维护数据,视图去逻辑上组织数据,存储过程提高性能,外键去关联不同表的记录,安全检查避免对敏感数据的未授权访问,对数据透明的加密解密的能力等等,还有很多隐藏的功能但是对于开发者来说非常的重要。

当你必须要将数据持久化的时候,关系数据库是你最好的选择,另一方面,当你必须要在一个应用程序中展现数据的时候,对象是最好的方式。相对于legacy dataset approach继承、封装和多态等特征能够允许以一个更好的编程风格去简化开发。

在我们深入EF细节之前,我们会利用本章的前三个部分去讨论如何从dataset approach 转移到object-based approach去 简化开发,养成使用ORM工具比如EF会跟以前有什么不同。

当你决定使用面向对象的时候,记住relation and object-orinted是不同的,EF的角色就是为了解决这个,让开发者能够关注业务的开发,忽略持久化方面,这些差异是很难克服的,在1.4你会发现去适应有很多的工作去做,在本章的最后部分,会想你展示EF如何实现我们的目标能够解决这个 差异并以一个简单的方式去访问数据。

最后你会有个很好的认识:ORM工具是什么,为什么我们在创建应用程序的时候应该考虑使用它。

 1.1 Getting Started With Data Access  从数据访问开始

表中的数据被存成很多行,每一行由很多列组成,这种高效的表格形式已经驱使开发人员在应用程序中展现数据好多年。ASP和VB开发人员使用recordsets从数据库中获取数据,recordset是一个泛型容器(感觉不准确),它能够以相同的方式组织获取的物理上以行列形式存储的数据,当.net出现的时候,开发人员有了一个新的对象去维护内存中的数据-dataset,尽管它跟recordset相比有很大不同,但是他们都有相同的purpose,组织数据的时候都有相同的行为--以行列的形式。

尽管这种representation在一些高级情况下是有效的,但是它缺少了很多特征比如类型安全,性能,可管理,在下一部分讨论dataset的时候,我们会说更多的细节。

在Java世界中,像dataset这种数据结构是一直存在的,但是现在已经不被推荐了,除了一些最简单的情况,在.Net世界,我们面对着同样的趋势,你可能会感到疑问:如果我不使用一般意义上的容器,那我该如何去展现数据?? 答案非常简单:对象。

在所有的情况下,对象都是优于dataset的,因为它不会受制于通用数据结构的限制。它提供了类型安全,Visual Studio中的自动补齐,编译时检查,更好的性能等等。我们将会在1.2讨论对象。

从使用对象获益是需要代价的,这源自于object-orinted paradigm(面向对象的范例?)和数据库使用的关系模型是不同的,这里有三点需要注意的不同处:

  • 关系:在一个表格话的结构中,你使用外键去关联,类使用引用去关联另一个类
  • 等价:在数据库中,数据能将一行与另一行区别开,然后在对象世界中,如果你有两个对象,有相同的类型相同的值,它们依然是不同的。
  • 继承:继承通常是使用在 面向对象的语言中,而在数据库中它是被支持的

这刚好接触到了对象关系不匹配这个问题,我们将会在1.4涉及到。

在这个大背景下,ORM负责对象的持久化,ORM位于应用程序和数据库之间,负责获取数据,并将其转化成对象,监视对象的变化,并将这种变化持久到数据库当中,这可以确保你不必写80%的数据访问层代码(来自于我们过往经验的判断)。

1.2 Developing applications using database-like structures

在过去的十年,我们一直使用VB,ASP,.Net等开发应用程序,几乎所有的语言都是使用额外的组件或者对象去访问数据库,并在内部维护数据,不仅在 每种语言里任务是相似的,更相似的是内部数据的呈现:数据被有行列概念的结构组织。结果就是应用程序和数据库以相同的方式去管理数据。

为什么不同的供应商会提供开发人员相同的编程模型?答案很简单:因为开发人员已经适应了这种表格化的展现,他们不需要再学习其他任何东西使其变的更productive.这些数据 结构 能够保存任何以行列形式表现的数据,甚至是来自于XML,WebService中的数据。。

因此供应商开发了一系列对象来展现数据,而不必让我们写一行代码,这些对象叫做数据容器。

1.2.1 Using datasets and data readers as data containers

在.Net之旅的开始,我们中的很多人都会使用dataset和datareader,用很少的代码,我们就可以将一个对象绑定到数据驱动的控件上,并且datareader提供了令人印象深刻的性能。通过数据适配器结合dataset,我们拥有一个完整的框架去读取更新数据,我们从来没有如此的富有创造性,Visual Studio也扮演了它的角色,通过集成的向导功能,使我们感觉到所有的东西都可以通过拖拽和很少的代码去创建。

让我们看一个糖炒栗子,假设你有一个Order表和一个OrderDetail表,如1.1所示,你需要创建一个简单的页面来显示所有的Orders.

 第一步是创建数据库连接,接着创建适配器,执行查询,将数据传到data table中来绑定到list控件,这些步骤在在下面列出来了。

using (SqlConnection conn = new SqlConnection(connString))
{
using (SqlDataAdapter da = new SqlDataAdapter("Select * from order",
conn))
{
DataTable dt = new DataTable();
da.Fill(dt);
ListView1.DataSource = dt;
ListView1.DataBind();
}
}

 通过重构,你可以从一个单独的方法中获得数据库连接和适配器,代码量又减少了,你所需要做的只是展示Order。

完成这个原型之后,顾客改变了细节,想要看到列表中每个订单的细节。解决方案变得更有挑战性,因为你有几种方案可供选择:

  • 从Order表获取数据,为每个订单查询细节  这个方案是目前最容易编程实现的,当一个订单绑定到 listview的时候,通过截取,我们可以查询到它的细节并展示。
  • 获取Order,OrderDetail表关联后的数据 这是表的笛卡尔乘积,跟OrderDetail有相同的记录,意味着结果集不能原样直接传递给控件,需要先传给本地处理
  • 通过两个查询获得Order和OrderDetail 这是目前最好的方案,因为它只对数据库做两次查询,当将订单绑定到控件的时候,可以在内存中过滤当前订单的信息并展示

不论你选择哪一个方案,都有重要的一点需要考虑:你不得不绑定数据结构,你的代码是被数据结构决定的,包括你获取的数据,每一个决定都会引向不同的代码,更换策略的时候会非常的头疼。

让我们继续,你的顾客现在需要一个页面去展示单独的订单,并且能够打印,这个页面必须包含标签去展示订单数据,并且有一个列表去展示订单细节,假设你是通过两个查询获取的数据,代码看起来是这样子的:

using (SqlConnection conn = new SqlConnection(connString))
{
using (SqlCommand cm = new SqlCommand("Select * from orderwhere orderid = 1", conn))
{
conn.Open();
using (SqlDataReader rd = cm.ExecuteReader())
{
rd.Read();
date.Text = ((DateTime)rd["OrderDate"]).ToString();
shippingAddress.Text = rd["ShippingAddress"].ToString();
shippingCity.Text = rd["ShippingCity"].ToString();
}
using (SqlDataReader rd = cm.ExecuteReader())
{
details.DataSource = rd;
details.DataBind();
}
}
}

这种访问数据的方式是完全不安全和普通的,但是从另一方面说,你有巨大的灵活性,你可以通过写通用代码(?)去实现这个功能,而不必关注表字段名和依赖配置,另一方面你失去了类型安全,你通过一个指定的string去识别字段,如果这个name值不正确,你会在运行期间得到异常。

现在你已经对通用的容器世界有了个大体了解,让我们继续研究它的限制,并且关注为什么在企业级程序中这种方案是终止的。

1.2.2 The strong coupling problem 强耦合问题

在前面的代码中你被问到选择哪种最好的方式去在一个列表里展示订单和订单细节,你所需要的只是一个订单列表,并和每个订单关联的细节。

DataReader和Data Table不允许你在不影响用户接口代码的情况下去获取数据,这意味着应用程序紧紧的耦合了数据结构,当数据结构发生变化的时候,你的应用程序代码也会有一个很大的变化,这也是这些对象不被推荐使用的最重要的原因,

即使你在 内存中有相同的数据,在大多数情况下,改变的是访问 数据层的代码而不是用户接口的代码

在我们过去参与的项目中,数据库是为一个程序服务的,所以数据被组织代码可以轻松的搞定,但这 不是经常的情况,有时候应用程序被创建在一个
已经存在的数据库上面,任何东西都不都不能被修改,因为其他应用正在使用数据库,在这种情况下,你会被更加紧密的耦合数据库和它的组织,会跟你的期盼有很大的差异,举个糖炒栗子,订单被存到一个表,商品寄送地址在另一张表,数据访问代码应该减少影响,但是这个烦人的问题依然存在。

当一个列 的 名字改变时会发生什么,这在应用程序开发期间经常发生,结果是接口代码需要适配反应这种变化,你的代码会变的非常的脆弱,因为查询和替换是达到这个目标的唯一方式,你可以 通过修改SQL来弱化这个问题,并且在结果集加别名来维护原来的名字,但是这回引起更多的困惑并产生一个新的问题。

1.2.3 The loose typing problem 松散的类型问题

获取datareader或者datatable中一列数据的值的时候,你通常是使用一个常量字符串,典型代码看起来是这个样子的:
object shippingAddress = orders.Rows[0]["ShippingAddress"];

变量shippingAddress是一个Object对象,所以它可以包含任何类型的数据你可能知道它包含的是一个string至,你不得不显示的做一个转换操作。

string shippingAddress = (string)orders.Rows[0]["ShippingAddress"];
string shippingAddress = orders.Rows[0]["ShippingAddress"].ToString();

Casting和转换都需要考虑到性能和内存使用,因为在将一个值类型转化成引用类型的使用内部转换会发生装箱和拆箱操作,
DataReader跟datatable相比有一个优势,它提供了指定的方法去访问字段而不需要显示的转化,这些方法接受一个integer参数代表着列在行中的索引位置,DataReader也有一个返回列索引的方法,给定列明,但是这会导致类型错误
C#
string address = rd.GetString(rd.GetOrdinal("ShippingAddress"));
string address = rd.GetString(rd.GetOrdinal("ShipingAdres")); //exception

上个部分讨论的列名变化引起的问题,编译期间会失去控制,运行期间不期望一个列明会发生变化,柔则你会弄错列明,编译器也不能帮助你避免错误,因为他们对列名一无所知。

 

1.2.4 The performance problem 性能问题

DataSet看起来是.net类库里最复杂的结构之一,它有一个 或者多个DataTable实例,每个DataTable有很多DataRow对象,每个DataRow对象是有很多DataColumn对象组成的,在一个DataTable中可以有一个主键,这个主键可以由一个或者多个列构成,并且可以 生命一个列作为外键跟另一个DataTable中的列关联,列支持版本,意味着如果你改变值,旧值和新值都会被存到裂伤做一个一致性检查,在法网数据库更新之前,你不得不使用DBDataAdapter类,另一个对象

尽管这些特征通常情况下是无用的并且被开发人员忽略,DataSet内部创建了这些对象的空的集合,这好像是一个让人忽略的资源浪费对于一个独立的程序来说,但是在一个多用户环境下,面对上千个请求,比如Web程序,这就会变的不可接受,优化数据库,修改SQL,添加hint提示等等都无用,如果你花费资源在你不需要的创建结构上。

比之下,DataReader是为不同的场景创建的,一个D他Table下载所有的数据从数据库读取到内存,并且经常你不需要内存中的所有数据,可以从数据库中一条一条的获取数据,另一个场景的数据更新,你经常需要读取数据,但不需要更新它,这种情况下,一些特征比如版本是无用的,DataReader是最后的选择(此场景下),因为它以只读方式获取数据(速度更快),尽管它提高了性能,忍让会遭受转换的问题,但是这点损失少于你从它获取的好处。

所有这些问题看起来是无法解决的,但是很多应用程序从数据库结构的使用中获得了好处,即使使用这些对象开发的时候没有问题,但是在企业级工程(代码大)你需要更多的控制和灵活性,你可以itihuidao基于面向对象的变成的好处,并且利用泪去组织你的数据。

1.3 Using classes to organize data 使用类去组织数据

我们生活在面向对象的年代,过程化语言依然存在,只不过被限定在特殊的环境下,比如COBOL在大型主机下的应用程序依然运行。
使用类是一个自然的选择对于当前的大多数程序,类是基于对象编程的基础,类可以非常容易的呈现数据,执行动作,使用类可以组织数据,发布事件等等,从数据组织的角度来说,类是通过方法和属性来表现数据的(属性说到底也是一些特殊的方法)。

通过使用类,你可以不必担心数据是如何持久化的而去表现数据,你不需要知道关于存储策略的任何东西,它可能是个数据库,Web服务,XML文件或是其他东西,表现数据的时候不用对存储策略需要了解被称为persistence ignorance,在这种方案下使用的类被称为POCOs(plain old clr objects).

在企业级应用中使用类会获得几个好处,有几点是特别重要的:

强类型:
你在获取row的每列值时,不必再做转换成正确的类型(或者至少,你不必在接口代码中做这些转换)
编译期间检查:
类通过暴露属性去访问数据,而不必使用通用的方法或者索引,你如果输入了不正确的属性名,立刻就会得到一个编译错误,而不必在运行时才发现这个错误

开发的简化:
像VS这样的编辑器提供智能感知加速开发,只能感知能提供给开发人员关于属性,事件方法等被类暴露出来的东东的提示,但是如果你使用DataSet,编辑器就不能帮助你了,因为列使用string获取数据,string是不受制于只能感知的。

存储无关的接口:
你不必为了适应数据库的结构而去改造类,这会给你最大程度的灵活性,类拥有自己的结构,尽管它经常跟它们关联的 表结构相似 ,但那时也不一定如此,你不必在关心数据的组织和获取,因为 你的代码对应着类,数据获取的细节委托给应用程序的特别部分,所以接口代码总是 能保持不变。

让我们在联系中看一下这些概念,重构前面部分提到的栗子。

1.3.1 Using classes to represent data  用类去表现数据

让我们再从头开始,顾客想在表格里面展示订单,第一步是创建一个订单类来保存订单数据(如图所示),订单类跟跟关联的数据库表有相同的结构,能观察到的唯一明显区别是你用.net中的类型(string,int32,datatime)代替了数据库中的字段类型。

第二步是创建一个类,拥有一个方法去从数据看读取数据并将其转换成对象,跟下面列出的一样,这个类经常被放在一个单独的程序集中,被称为数据层。

public List<Order> GetOrders()
{
using (SqlConnection conn = new SqlConnection(connString))
{
using (SqlCommand comm = new SqlCommand("select * from orders", conn))
{
conn.Open();
using(SqlDataReader r = comm.ExecuteReader())
{
List<Order> orders = new List<Order>();
while (rd.Read())
{
orders.Add(
new Order
{
CustomerCode = (string)rd["CustomerCode"],
OrderDate = (DateTime)rd["OrderDate"],
OrderCode = (string)rd["OrderCode"],
ShippingAddress = (string)rd["ShippingAddress"],
ShippingCity = (string)rd["ShippingCity"],
ShippingZipCode = (string)rd["ShippingZipCode"],
ShippingCountry = (string)rd["ShippingCountry"]
}
);
}
return orders;
}
}
}
}
...
ListView1.DataSource = new OrderManager().GetOrders();
ListView1.DataBind();

"这么多的代码",这经常是人们面对上面代码的第一反应,是的,确实有很多代码,尤其是跟1.1的比较(使用DataSet),如果你的应用程序是展示像这样简单的数据,dataset的方案是更好的,但是当事情变得负责,类能帮助你更多,让我们看一下第二个需要的特征:在一个Form里面展示一个单独的订单,当这个订单获取到时,使用类去展示数据显得更加直接。

shippingAddress.Text = order.ShippingAddress;
shippingCity.Text = order.ShippingCity;

最后一步是在一个grid里面展示order和它关联的细节,要做到这些需要更深的知识(理解?),因为它引入了模型的概念,你不能用一个单独的类去展示订单和它的细节,你不得不使用两个分开的类(跟表一样),在下一部分,我们会讨论这项技术。

1.3.2 From a single class to the object model  从一个单独的类到对象模型

你现在已经看到如何开发一个单独的类,并用从数据库中获取的数据去创建它,但是,当你创建更多的类,并且彼此之间有联系的时候,你会体会到真正的力量。
在数据库中,Order和它的细节关系是通过外键约束,从数据库的观点来看,这是个正确的方案。
在面向对象的世界中,你不得不遵守另一个path(观点??),在创建OrderDetail类的时候,给它一个OrderID属性是没用的,最好的解决方案是充分利用类的特性:他们可以拥有自定义类型的属性。这 意味着Order可以添加对OrderDetail的引用,OrderDetail也可以拥有一个对Order的引用。
当你创建这些关系的时候,你同时创建了一个对象模型,一个对象模型是相互关联的类的集合。
当你需要在一个单独的表格里面展现Order和OrderDetail的时候,对象模型的真正能力就浮现出来了。在1.2.1列出了一系列解决方案,因此有一个烦人的问题:每个都不同,但是更糟糕的是,每个都需要不同的代码在接口上。使用类,你的接口代码就跟这些烦人的问题完全隔离开了,因为你不再需要关系数据库,应用程序的一部分会获取数据返回对象,这样,使用对象就实现了存储无关的接口。

对象模型和领域模型模式经常被考虑做同样的事情,他们最开始看起来貌似是极其相像的,因为他们都保存着从存储中抽取的数据,但是当我们仔细挖掘一下,你会发现它们是不同的:对象模型仅仅只保存数据,而领域模型除了保存数据还暴露行为。
我们一直研究的Order类是对象模型的一个完美表现,除了拥有数据的属性,没有其他东西,你可以再给它增加一个计算属性用来报告完整的地址通过将其他属性值包含起来,但这只是个帮助方法,并没有添加任何行为到这个类。
如果你想从一个对象模型迁移到领域模型,你必须给这个类添加行为,为了更好的理解行为这个概念,假设你需要知道一个订单是否超出了允许的总数,使用对象模型,你不得不在另一个类里面添加一个方法,在这个方法中调用数据库获得允许的最大总数,然后跟订单数比较,如果你选择领域模型,你可以天津一个IsCorrent方法给Order类,去执行这个检查操作,这种方式下你就添加了一个行为和表现给Order对象。
创建和维护一个领域对象并不轻松,他会强制软件架构对应用的设计做出选择,特别是类必须自己负责验证,必须保持在一个有效的状态(比如 ,订单必须有一个关联的用户),这可能会导致代码膨胀,所以为了避免这个宽饶,你不得不创建另一个类来负责验证,并在领域模型中保存这些类。
对象模型和领域模型模式的细节已经超出了这本书的范围,我们不会再设计,但是有大量的关于则个话题的书,我们推荐 Domain Driven Design这本书(Eric Evans).

现在看来这个例子已经被简化了,你可能注意到订单类有一个CustomerID属性,OrderDetail类有一个ProductID属性,在一个完整的设计中,你也会有一个Customer,Product类,一个顾客会拥有一个适当的折扣在模型条件下,一个产品属于一个类,出啊关键一个强壮的对象模型需要更高的理解,准则,和一定的联系。
乍看之下,u类和数据库表之间一对一的映射已经足够了,但是再往下,面向对象拥有许多的表现和他正是跟数据库结构不同的,继承,多对多的关系,逻辑上的数据组都会影响你设计一个模型,更重要的是,这些特征在关系和模型自建创建了一个不匹配,在学术界,就是众所周知的object/relational mismatch,在下一部分讨论。



posted @ 2016-07-27 10:37  大侠的哥哥是菜鸟  阅读(170)  评论(0编辑  收藏  举报