LINQ之路 9:LINQ to SQL 和 Entity Framework(上)

时间:2023-03-08 15:57:31

在上一篇中,我们从理论和概念上详细的了解了LINQ的第二种架构“解释查询”。在这接下来的二个篇章中,我们将使用LINQ to SQL和Entity Framework来实践“解释查询”,学习这些技术的关键特性。在本系列文章中,我不准备事无巨细的讨论LINQ to SQL和Entity Framework的方方面面,毕竟那样需要太多的篇幅,也会让我们从LINQ上面转移注意力,况且,园子里也有不少介绍LINQ to SQL和Entity Framework的好文章。我们在此关注的是LINQ to SQL和Entity Framework中的”LINQ”部分,并会比较这两种技术的相同和不同之处。通过我们之前介绍的LINQ知识还有将来会讨论的更多LINQ Operators,相信阅者能针对LINQ to SQL和Entity Framework写出优雅高效的查询。为了简单清晰,文中有些地方对LINQ to SQL和Entity Framework进行了缩写,分别为:L2S和EF。

LINQ to SQL和Entity Framework之关联

LINQ to SQL和Entity Framework都是一种包含LINQ功能的对象关系映射技术。他们之间的本质区别在于EF对数据库架构和我们查询的类型实行了更好的解耦。使用EF,我们查询的对象不再是完全对应数据库架构的C#类,而是更高层的抽象:Entity Data Model。这为我们提供了额外的灵活性,但是在性能和简单性上面也会有所损失。

LINQ to SQL由C#团队开发并在.NET Framework 3.5中发布,而Entity Framework由ADO.NET团队开发并作为.NET Framework 3.5 Service Pack 1的一部分发布。此后,LINQ to SQL由ADO.NET团队接手,其结果是:在.NET 4.0中,ADO.NET团队更加专注于EF的改进,相对来说,LINQ to SQL的改进要小得多。

LINQ to SQL和Entity Framework各有所长,LINQ to SQL是一个轻量级的ORM框架,旨在为Microsoft SQL Server数据库提供快速的应用程序开发,其优点是易于使用、简单、高性能。而Entity Framework的优点在于:其为创建数据库架构和实体类之间的映射提供了更好的灵活性,它还通过提供程序支持除了SQL Server之外的第三方数据库。

EF 4.0一个非常受欢迎的改进是它现在支持与LINQ to SQL几乎同样的查询功能。这意味着我们在系列文章中的LINQ-to-db查询可以同时适用于EF 4.0和L2S。而且,这也使得L2S成为我们学习使用LINQ查询数据库的理想技术,因为其保持了对象关系方面的简单性,并且我们学习到的查询原则和技术同样适用于EF。

LINQ to SQL实体类

L2S 允许我们使用任何类来表示数据,只要我们为类添加了合适的Attribute(特性)装饰,比如:

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

[Table] 特性定义在System.Data.Linq.Mapping名字空间中,它告诉L2S该类型的对象代表了数据库表里的一行数据。默认情况下,它假设表名和类名相同,当他们不同时,我们就可以指定具体的表名,如下:

[Table (Name="Customers")]

L2S把这种经过[Table]特性装饰的类成为实体类。一个实体类的结构必须匹配它表示的数据库表,这样才能生成可以正确执行的SQL脚本。

[Column] 特性指定一个字段或属性映射到数据库表的一列,如果列名与字段名/属性名不相同,我们可以指定具体的映射列名:

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

我们可以在[Column]特性中指定IsPrimaryKey属性表示该列为表的主键,这对于保持对象标识、往数据库写入更新是必须的。

除了直接定义public字段,我们也可以定义private字段和public属性,这样我们就能在属性存取时加入验证逻辑。此时,为了性能考虑,我们可以告诉L2S当从数据库存取数据时,绕过属性存取器而直接将值写入private字段。当然,前提是我们认为数据库中的值是正确的,不需要经过属性存取器中的验证逻辑。

        private string name = string.Empty;

        // Column(Storage = "name") 告诉L2S当从数据库生成实体时直接将数据写入name字段,而不通过set访问器
[Column(Storage = "name")]
public string Name
{
get { return name; }
set { if(value.Length > ) name = value; }
}

可以看到,在使用LINQ to SQL时,我们首先要参照数据库的结构来创建各种必须的实体类,这当然不是一种令人愉快的事情。好在,我们可以通过Visual Studio(新增一个”LINQ to SQL Classes” Item)或SqlMetal命令行工具来自动生成实体类。

Entity Framework实体类

和LINQ to SQL一样,Entity Framework允许我们使用任何类来表示数据(尽管我们必须实现特定的接口来完成诸如导航属性等功能)。比如,下面的EF实体类表示一个customer,它被映射到数据库的customer表:

    [EdmEntityType (NamespaceName="EFModel", Name="Customer")]
public partial class Customer
{
[EdmScalarProperty(EntityKeyProperty = true, IsNullable= false )]
public int ID { get; set; } [EdmScalarProperty(EntityKeyProperty = false, IsNullable = false)]
public string Name { get; set; }
}

但和L2S不同的是,这样的一个类并不能独立工作。记住:在使用EF时,我们并不是直接查询数据库,而是查询一个更高层的模型,该模型叫做Entity Data Model(EDM)。所以我们需要某种方法来描述EDM,这通常是由一个以.edmx为扩展名的XML文件来完成的,它包含了一下三个部分:

  • 概念模型,用来描述EDM并且和数据库完全隔离
  • 存储模型,用来描述数据库架构
  • 映射规范,用来描述概念模型如何映射到存储模型

创建一个.edmx文件的最简单方法是在Visual Studio中添加一个”ADO.NET Entity Data Model” 项目,然后按照向导提示来从数据库生成EDM。这种方法不但生成了.edmx文件,还为我们生成了实体类,EF中的实体类对应EDM的概念模型。

设计器为我们生成的EDM初始时包含了表和实体类之间简单的1:1映射关系,当然,我们可以通过设计器或编辑.edmx文件来调整我们的EDM。下面就是我们可以完成的一些工作:

  • 映射多个表到一个实体
  • 映射一个表到多个实体
  • 通过ORM领域流行的三种标准策略来映射继承的类型

这三种继承策略包括:

  • 表到层次类型(Table per hierarchy):单个表映射到一个完整的类继承层次结构。表中的一个类型辨别列用来指示每一行数据应该映射到何种类型。
  • 表到类型(Table per type):单个表映射到单个类型,这意味着继承类型会被映射到多个表。当我们查询一个entity时,EF通过生成SQL JOIN来合并所有的基类型。
  • 表到具体类型(Table per concrete type):单独的表映射到每个具体类型,这意味着一个基类型映射到多个表,当我们查询基类型的entity时,EF会生成SQL UNION来合并数据。

DataContext和ObjectContext

一旦我们定义好了实体类(EF还需定义EDM),我们就可以开始使用LINQ进行查询了。第一步就是通过制定连接字符串来初始化一个DataContext(L2S)或ObjectContext(EF)。

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

需要了解的是,直接初始化DataContext/ObjectContext是一种底层的访问方式,但它很好的说明了这些类的工作方式。通常情况下,我们会使用类型化的context(通过继承DataContext/ObjectContext),详细情况稍后就会讨论。

对于L2S,我们传入数据库连接字符串;而对于EF,我们需要传入实体(entity)连接字符串,它同时包含了数据库连接字符串和查找EDM的额外信息。如果你通过Visual Studio创建了EDM,你会在app.config文件中找到针对该EDM的实体连接字符串,比如:

  <connectionStrings>
<add name="testEntities" connectionString="metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string=&quot;data source=localhost;initial catalog=test;integrated security=True;multipleactiveresultsets=True;App=EntityFramework&quot;" providerName="System.Data.EntityClient"/>
</connectionStrings>

之后我们就可以通过调用GetTable(L2S)或CreateObjectSet(EF)来获得查询对象。下面的示例使用了我们上面创建的Customer实体类:

            var context = new DataContext("Data Source=LUIS-MSFT; Initial Catalog=test; Integrated Security=SSPI;");
Table<Customer> customers = context.GetTable<Customer>(); Console.WriteLine(customers.Count()); // 表中的行数
Customer cust = customers.Single(c => c.ID == ); // 获取ID为1的Customer

下面是EF中的等价代码,可以看到,除了Context的构建和查询对象的获取有所不同,后面的LINQ查询都是一样的:

            var context = new ObjectContext(ConfigurationManager.ConnectionStrings["testEntities"].ConnectionString);
context.DefaultContainerName = "testEntities";
ObjectSet<Customer> customers = context.CreateObjectSet<Customer>(); Console.WriteLine(customers.Count()); // 表中的行数
Customer cust = customers.Single(c => c.ID == ); // 获取ID为1的Customer

一个DataContext/ObjectContext对象有两个作用。其一是工厂作用,我们通过它来创建查询对象,另外,它会跟踪我们对entity所做的任何修改,所以我们可以把修改结果保存到数据库。下面的代码就是更新customer的示例:

            // Update Customer with L2S
Customer cust = customers.OrderBy(c => c.Name).First();
cust.Name = "Updated Name";
context.SubmitChanges(); // Update Customer with EF, Calling SaveChanges instead
Customer cust = customers.OrderBy(c => c.Name).First();
cust.Name = "Updated Name";
context.SaveChanges();

强类型contexts

任何时候都去调用GetTable<>()或CreateObjectSet<>()并不是一件让人愉快的事情,一个更好的方式是为特定的数据库创建DataContext/ObjectContext的子类,在子类中为各个entity添加属性,这就是强类型的context,如下:

    class LifePoemContext : DataContext
    {
public LifePoemContext(string connectionString) : base(connectionString) { } public Table<Customer> Customers
{
get { return GetTable<Customer>(); }
} //... 为其他table创建相应的属性
} // Same thing for EF
class LifePoemContext : ObjectContext
    {
public LifePoemContext(EntityConnection connection) : base(connection) { } public ObjectSet<Customer> Customers
{
get { return CreateObjectSet<Customer>(); }
} //... 为其他table创建相应的属性
}

之后,我们就可以通过使用属性来写出更加简洁优雅的代码了:

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

如果你是使用Visual Studio来创建”LINQ to SQL Classes”或”ADO.NET Entity Data Model”,它会自动为我们生成强类型的context。设计器同时还会完成其他的工作,比如对标识符使用复数形式,在我们的例子中,它是context.Customers而不是context.Customer,即使SQL表名和实体类都叫Customer。

对象跟踪/Object tracking

一个DataContext/ObjectContext实例会跟踪它创建的所有实体,所以当你重复请求表中相同的行时,它可以给你返回之前已经创建的实体。换句话说,一个context在它的生存期内不会为同一行数据生成两个实例。你可以在L2S中通过设置DataContext对象的ObjectTrackingEnabled属性为false来取消这种行为。在EF中,你可以基于每一种类型进行设置,如:context.Customers.MergeOption = MergeOption.NoTracking; 需要注意的是,禁用Object tracking同时也会阻止你想数据库提交更新。

为了说明Object tracking,假设一个Customer的名字按字母排序排在首位,同时它的ID也是最小的。那么,下面的代码,a和b将会指向同一个对象:

            var context = new testEntities(ConfigurationManager.ConnectionStrings["testEntities"].ConnectionString);
Customer a = context.Customers.OrderBy(c => c.Name).First();
Customer b = context.Customers.OrderBy(c => c.ID).First();
Console.WriteLine(object.ReferenceEquals(a, b)); // output: True

这会导致几个有意思的结果。首先,让我们考虑当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从缓存中返回的的对象只有部分属性被赋值。

关联/Associations

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

      Create 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属性(位于关联的另一边):

        // With L2S
[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的方式来工作,当我们设置如下属性后,EFEntityCollectionsEntityReference就会自动实现延迟装载,而不需要明确调用其Load方法。

        context.ContextOptions.LazyLoadingEnabled = true;

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