zoukankan      html  css  js  c++  java
  • LINQ之路 9:LINQ to SQL 和 Entity Framework(上)

    系列博客导航:

    LINQ之路系列博客导航

    LINQ之路 1:LINQ介绍

    LINQ之路 2:C# 3.0的语言功能(上)

    LINQ之路 3:C# 3.0的语言功能(下)

    LINQ之路 4:LINQ方法语法

    LINQ之路 5:LINQ查询表达式

    LINQ之路 6:延迟执行(Deferred Execution)

    LINQ之路 7:子查询、创建策略和数据转换

    LINQ之路 8:解释查询(Interpreted Queries)

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

    LINQ之路10:LINQ to SQL 和 Entity Framework(下)

    LINQ之路11:LINQ Operators之过滤(Filtering)

    LINQ之路12:LINQ Operators之数据转换(Projecting)

    LINQ之路13:LINQ Operators之连接(Joining)

    LINQ之路14:LINQ Operators之排序和分组(Ordering and Grouping)

    LINQ之路15:LINQ Operators之元素运算符、集合方法、量词方法

    LINQ之路16:LINQ Operators之集合运算符、Zip操作符、转换方法、生成器方法

    LINQ之路17:LINQ to XML之X-DOM介绍

    LINQ之路18:LINQ to XML之导航和查询

    LINQ之路19:LINQ to XML之X-DOM更新、和Value属性交互

    LINQ之路20:LINQ to XML之Documents、Declarations和Namespaces

    LINQ之路21:LINQ to XML之生成X-DOM(Projecting)

    LINQ之路系列博客后记

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



  • 相关阅读:
    [转]HSPICE 使用流程
    [转]到底怎么样才叫看书?——上篇
    js弹出层
    mvc自定义扩展控件
    yield 关键字
    mvc学习地址
    C# 将List中的数据导入csv文件中
    Asp.net 下载文件(弹出对话框的形式)
    Asp.net中用Jquery实现Ajax回调后台方法
    SharePoint中获取当前登录的用户名
  • 原文地址:https://www.cnblogs.com/xwj517537691/p/3126904.html
Copyright © 2011-2022 走看看