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


    在上一篇中,我们从理论和概念上详细的了解了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 > 5) 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 == 1); // 获取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 == 1); // 获取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技术的更多细节和值得关注的地方。

  • 相关阅读:
    使用 Dockerfile 定制镜像
    UVA 10298 Power Strings 字符串的幂(KMP,最小循环节)
    UVA 11090 Going in Cycle!! 环平均权值(bellman-ford,spfa,二分)
    LeetCode Best Time to Buy and Sell Stock 买卖股票的最佳时机 (DP)
    LeetCode Number of Islands 岛的数量(DFS,BFS)
    LeetCode Triangle 三角形(最短路)
    LeetCode Swap Nodes in Pairs 交换结点对(单链表)
    LeetCode Find Minimum in Rotated Sorted Array 旋转序列找最小值(二分查找)
    HDU 5312 Sequence (规律题)
    LeetCode Letter Combinations of a Phone Number 电话号码组合
  • 原文地址:https://www.cnblogs.com/qixuejia/p/5410658.html
Copyright © 2020-2023  润新知