C#学习笔记(八)—–LinqToSql和Entity Framework(上)

来源:互联网 发布:非广播多路访问网络 编辑:程序博客网 时间:2024/05/21 17:19

LinqToSql和Entity Framework

虽然linqtosql(以下简称L2S)现在已经停止更新或者更新的很慢,它的地位也由Entity Framework(以下简称EF)逐渐取代,但是理解它们的原理对于后面的深入学习EF还是非常有必要的,而且基于L2S的速度上面的优势,有时也会直接用L2S来做业务。
本章还是基于前面讲解的Linq的两个方面(本地查询和解释查询)中的解释查询来介绍的,在这一节中介绍这两个方法的核心技术特性。

L2S和EF的区别

L2S和EF都是用linq来实现的对象映射的工具。他们之间的不同在于映射的方式,我们知道,在数据库查询中,映射的一端是数据库表,L2S可以将数据库表的结构映射成对象,然后供调用者使用,这种映射严格按照数据库表结构。与此不同的是,Ef对这种映射做了一些改进,那就是允许我们定义实体类,也就是允许开发者定义数据库表被映射成什么类型。EF提供的方案更加灵活,但是它会降低查询的效率,也增加了使用的复杂度,应为需要额外的时间去维护数据库和自定义实体类之间的映射关系。
尽管Ef在后续的版本中不断的改进,但是两种技术还是各有优势。L2S的优点是简单易用、执行性能好,此外他生成的SQL的解释质量更好一些,EF的优点是允许我们创建自定义的持久化的实体类,用于数据库的映射。另外Ef允许使用同一个查询机制查询SQL Server之外的数据源,实际上L2S也支持这个功能,但是为了鼓励第三方的查询机制的出现,它没有对外公布这些机制。

L2S的实体类

L2S的实体类使用table和column等标签(attribute)来对创建的实体类进行标识,下面例子:

[Table]public class Customer{[Column(IsPrimaryKey=true)]public int ID;[Column]public string Name;}

table标签定义在System.Data.Linq.Mapping命名空间中,它定义的类型用来承载数据表中的一行数据,默认情况下,L2S会认为这个类名和它对应的表明是相同的,如果想让两者不同的话,由于表明已经固定,只能更改对应的类名,更改方式是在table标签中显示的指定类名:

[Table (Name="Customers")]

如果一个类具有table标签,就称这个类是实体类。为了能够顺利使用,这个实体(entity)的结构必须与数据表中的结构相匹配,多字段或少字段都不行。这种限制使得这种映射是一种低级别的比较low的映射,试想一下有一张数据表中有一百多个字段的情形。
column标签用来指示数据表中的某一列,如果实体中定义的列名和数据表中的别名不同,那么需要在column中特别之处对应的列名,如下面:

[Column (Name="FullName")]public string Name;

column标签中的IsPrimaryKey属性用于指示当前列是主键,在数据库中这列用于唯一标识一条数据。在程序中也用于这列区分不同的实体,将实体中的变化写入数据库的时候,也需要使用这列来确定写入的目标。
在实体类中,除了可以定义公共属性为,我们还可以定义后备字段来对应数据库中的列。因此我们可以在属性的set结构中定义判断逻辑,用于检查能否给这个字段赋值,而从数据库读取数据的时候,则直接将数据赋值给私有字段即可。下面:

string _name;[Column (Storage="_name")]public string Name { get { return _name; } set { _name = value; } }

Column(Storage=”_name”)告诉L2S在读取数据库的时候,直接向 _name中赋值即可,不用通过Name属性。总的来讲,在定义实体类的时候,L2S允许将数据库中的字段映射对象定义成私有的,他可以访问实体类中的私有变量。

EF的实体类

和L2S中的实体类相似,Ef中允许开发者定义自己的实体类用于承载数据,不同的是,Ef中的实体类的定义要灵活得多,在理论上允许任何类型的类来作为实体类使用(在某些情况下需要实现一些接口,例如定义导航属性的时候),也就是说实体类中的结构不用于和数据表中的字段完全对应。下面是一个Customer表对应的实体类的代码段,这段代码基本上是的和数据库表结构对应的:

// You'll need to reference System.Data.Entity.dll[EdmEntityType (NamespaceName = "NutshellModel", Name = "Customer")]public partial class Customer{[EdmScalarPropertyAttribute (EntityKeyProperty = true, IsNullable = false)]public int ID { get; set; }[EdmScalarProperty (EntityKeyProperty = false, IsNullable = false)]public string Name { get; set; }}

和L2S不同的是,在Ef中,要完成数据的映射和查询,之定义上面这个实体类是不够的。因为在Ef中,查询并不是直接针对数据库进行的,它使用了一种更高级别的抽象模型,称为实体数据模型(EDM, entity data model),我们的查询语句是针对这个模型来定义的。EDM实际上是使用XML定义的一个.edmx类型的文件,这个文件包含三个部分:
①概念模型:定义了数据库的信息,不同的数据库有不同的概念模型内容。
②存储模型:定义了数据库的表结构。
③映射:定义了数据库表和实体类之间的映射关系。
创建.edmx文件最简单的方式是使用visual studio,在项目菜单中点击添加新项,在弹出的窗口中选择ADO.NET Entity Data Model(实体模型)。之后使用向导就可以完成实体类到数据表的映射配置。这一席里操作不仅添加一个edmx文件,还会创建设计到的实体类。
Ef的设计者假设你最初想要的映射关系是比较简单的1:1,尽管这样,如果确实需要这种特殊的映射关系,还是可通过修改edmx文件中的相关内容来实现,下面是几个常用的操作:
①多个表映射到一个实体类
②一个表映射到多个实体类
③在ORM中有三种方式将继承关系的类映射到表,这三中方式分别是:
这里写图片描述

DataContext和ObjectContext

在创建了实体类之后(EF还需要有EDM文件),就可以对数据库进行查询了,在查询之前,首选要创建DataContext(L2S)或者ObjectContext(EF)对象,这个对象用于指定数据库连接字符串。

var l2sContext = new DataContext ("database connection string");var efContext = new ObjectContext ("entity connection string");

**提示:E**f中的连接字符串可由创建edmx的时候自动导入程序的配置文件中,如app.config。
直接创建上面这两个类型的实例是一种很底层的使用方式,它可以展示出这两种类型是如何工作的,但是实际应用中,更常用的方式是创建类型化的context(继承自DataContext或ObjectContext)来使用,他的使用方式在接下来的内容中简要介绍。
然后我们就可以使用GetTable(L2S)或者CreateObjectSet(EF)对象了。这两个对象都是用于从数据库中读取数据,下面是两个简单的示例,演示这两个对象如何使用:

//L2S:var context = new DataContext ("database connection string");Table<Customer> customers = context.GetTable <Customer>();Console.WriteLine (customers.Count()); // # of rows in table.Customer cust = customers.Single (c => c.ID == 2); // Retrieves Customer// with ID of 2.

下面的是EF的示例:

var context = new ObjectContext ("entity connection string");context.DefaultContainerName = "NutshellEntities";ObjectSet<Customer> customers = context.CreateObjectSet<Customer>();Console.WriteLine (customers.Count()); // # of rows in table.Customer cust = customers.Single (c => c.ID == 2); // Retrieves Customer// with ID of 2.

这两个对象实际上只做两件事情:第一,它作为一个工厂,提供我们用于查询数据库的对象。第二,它会维护实体类的状态,如果查询出的实体类中的值在类外改变了,它会记录下这个字段,然后便于更新数据库。下面我们接着上面的代码完成数据的更新操作:

Customer cust = customers.OrderBy (c => c.Name).First();cust.Name = "Updated Name";context.SubmitChanges();

在EF中,唯一不同的是使用SaveChanges方法代替SubmitChanges方法:

Customer cust = customers.OrderBy (c => c.Name).First();cust.Name = "Updated Name";context.SaveChanges();

类型化的contexts

在对数据库的查询中,每次都调用GetTable<Customer>()或者CreateObjectSet<customer>()显然不是一个好的选择,一个更好的方式是为每个数据库定义一个继承自上面两个类的子类,在为这个子类添加一个这样的属性,这种属性我们称之为类型化的contexts:

class NutshellContext : DataContext // For LINQ to SQL{public Table<Customer> Customers{get { return GetTable<Customer>(); }}// ... and so on, for each table in the database}

EF:

class NutshellContext : ObjectContext // For Entity Framework{public ObjectSet<Customer> Customers{get { return CreateObjectSet<Customer>(); }}// ... and so on, for each entity in the conceptual model}

然后你就可以很轻松的这样来调用:

var context = new NutshellContext ("connection string");Console.WriteLine (context.Customers.Count());

如果通过Visual Studio在项目中创建LINQ to SQL Classes或者添加ADO.NET Entity Data Model,vs会自动创建这种类型化的context,除此之外设计器还会做一些额外的工作,例如将标识符复数化(要在添加的时候选中这个功能),所谓复数化,具体示例来讲,就是在使用Customer实体的时候,应该以复数的形式调用context.Cuctomers,而不是context.Customer,即使数据库表和实体类都是单数Customer,系统也会自动使用复数。

销毁DataContext/ObjectContext对象

尽管这两个类都实现了IDisposable接口,而且Disoose方法会强制断开数据库连接,但是我们一般不通过调用Dispose方法来销毁这两个对象,因为这两个对象在返回查询结果后会自动断开连接。实际上销毁这两个对象会带来一些问题,例如下面这示例:

IQueryable<Customer> GetCustomers (string prefix){using (var dc = new NutshellContext ("connection string"))return dc.GetTable<Customer>().Where (c => c.Name.StartsWith (prefix));}...foreach (Customer c in GetCustomers ("a"))Console.WriteLine (c.Name);

上面这段代码会抛出异常:
这里写图片描述
这将会失败,因为当我们枚举这个方法的值时,查询已经被计算(而且已经自动Dispose并释放掉DataContext了)。
解决的方法当然是把uising语句去掉了。

对象状态跟踪

一个DataContext/ObjectContext实例会跟踪它创建的所有实体,所以当你重复请求表中相同的行时,它可以给你返回之前已经创建的实体。换句话说,一个context在它的生存期内不会为同一行数据生成两个实例。你可以在L2S中通过设置DataContext对象的ObjectTrackingEnabled属性为false来取消这种行为。在EF中,你可以基于每一种类型进行设置,如:context.Customers.MergeOption = MergeOption.NoTracking; 需要注意的是,禁用Object tracking同时也会阻止你想数据库提交更新。为了说明Object tracking,假设一个Customer的名字按字母排序排在首位,同时它的ID也是最小的。那么,下面的代码,a和b将会指向同一个对象:

context.Customers.MergeOption = MergeOption.NoTracking;

关闭对象的状态跟踪功能之后,为了数据的安全,通过context向数据库中提交更新的功能也同时被禁用。
为了说明对象状态跟踪的工作方式,假设一个customer的名字是字母表中的第一个字母a并且他的id也是数据库中最小的。那么在下面这个示例中,a和b两个对象指向的就是同一个内存对象

var context = new NutshellContext ("connection string");Customer a = context.Customers.OrderBy (c => c.Name).First();Customer b = context.Customers.OrderBy (c => c.ID).First();

这会导致几个有意思的结果。首先,让我们考虑当L2S或EF在遇到第二个query时到底会发生什么。它从查询数据库开始,然后获取ID最小的那一行数据,接着就会从该行读取主键值并在context的实体缓存中查找该主键。如果找到,它会直接返回缓存中的实体而不更新任何值。所以,如果在这之前其他用户更新了该Customer的Name,新的Name也会被忽略。这对于防止意外的副作用和保持一致性至关重要,毕竟,如果你更新了Customer对象但是还没有调用SubmitChanges/SaveChanges,你是不会希望你的更新会被另外一个查询覆盖的吧。
第二个结果是在你不能明确把结果转换到一个实体类形,因为在你只选择一行数据的部分列时会引起不必要的麻烦。例如,如果你只想获取Customer的Name时:

            // 下面任何一种方法都是可行的            context.Customers.Select(c => c.Name);            context.Customers.Select(c => new { Name = c.Name } );            context.Customers.Select(c => new MyCustomerType { Name = c.Name } );            // 但下面这种方法会引起麻烦            context.Customers.Select(c => new Customer { Name = c.Name });

下面那种方法会抛出异常:
这里写图片描述
原因在于Customer实体只是部分属性被获取,这样下一次如果你查询Customer的所有列时,可是context从缓存中返回的的对象只有部分属性被赋值。

关联Association

实体生成工具还为我们完成了一项非常有用的工作。对于我们定义在数据库中的每个关联(relationship),它会在关联的两边添加恰当的属性,让我们可以使用关联来进行查询。比如,假设Customer和Order表存在一对多的关系:

//SQLCreate table Customer      (            ID int not null primary key,            Name varchar(30) not null      )      Create table Orders      (            ID int not null primary key,            CustomerID int references Customer (ID),            OrderDate datetime,            Price decimal not null      )

通过自动生成的实体类形,我们可以写出如下的查询:

//获取第一个Custoemr的所有Orders            Customer cust1 = context.Customers.OrderBy(c => c.Name).First();            foreach (Order o in cust1.Orders)                Console.WriteLine(o.Price);             //获取订单额最小的那个Customer            Order lowest = context.Orders.OrderBy(o => o.Price).First();            Customer cust2 = lowest.Customer;

并且,如果cust1和cust2正好是同一个Customer时,他们会指向同一对象:cust1 == cust2会返回true。

让我们来进一步查看Customer实体类中自动生成的Orders属性的签名:

// With L2S        [Association(Name="Customer_Order", Storage="_Orders", ThisKey="ID", OtherKey="CustomerID")]              public EntitySet<Order> Orders { get {...} set {...} }        // With EF        [EdmRelationshipNavigationProperty("testModel", "FK__Orders__Customer__45BE5BA9", "Orders")]        public EntityCollection<Order> Orders { get {...} set {...} }

一个EntitySet或EntityCollection就如同一个预先定义的query,通过内置的Where来获取相关的entities。[Association]特性给予L2S必要的信息来构建这个SQL query;[EdmRelationshipNavigationProperty]特性告知EF到EDM的何处去查找当前关联的信息。

和其他类型的query一样,这里也会采用延迟执行。对于L2S,一个EntitySet会在你对其进行枚举时生成;而对于EF,一个EntityCollection会在你精确调用其Load方法时生成。

下面是Orders.Customer属性(位于关联的另一边):

 [Association(Name="Customer_Order", Storage="_Customer", ThisKey="CustomerID", OtherKey="ID", IsForeignKey=true)]        public Customer Customer { get {...} set {...} }

尽管属性类型是Customer,但它底层的字段(_Customer)却是EntityRef类型的:private EntityRef<Customer> _Customer; EntityRef实现了延迟装载(deferred loading),所以直到你真正使用它时Customer才会从数据库中获取出来。

EF以相同的方式工作,不同的是你必需调用EntityReference对象的Load方法来装载Customer属性,这意味着EF必须同时公开真正的Customer对象和它的EntityReference包装者,如下:

 // With EF        [EdmRelationshipNavigationProperty("testModel", "FK__Orders__Customer__45BE5BA9", "Customer")]        public Customer Customer { get {...} set {...} }        public EntityReference<Customer> CustomerReference

我们也可以让EF按照L2S的方式来工作,当我们设置如下属性后,EF的EntityCollections和EntityReference就会自动实现延迟装载,而不需要明确调用其Load方法。

 context.ContextOptions.LazyLoadingEnabled = true;

在下一篇LINQ to SQL和Entity Framework(下)中,我们会讨论学习这两种LINQ-to-db技术的更多细节和值得关注的地方。

阅读全文
0 0
原创粉丝点击