Entity Framework 4.1 Code-First 学习笔记
- 2019 年 10 月 7 日
- 筆記
CodeFirst提供了一种先从代码开始工作,并根据代码直接生成数据库的工作方式。Entity Framework 4.1在你的实体不派生自任何基类、不添加任何特性的时候正常的附加数据库。另外呢,实体的属性也可以添加一些标签,但这些标签不是必须的。下面是一个简单的示例:
publicclass Order { publicint OrderID { get; set; } publicstring OrderTitle { get; set; } publicstring CustomerName { get; set; } public DateTime TransactionDate { get; set; } public List<OrderDetail> OrderDetails { get; set; } } publicclass OrderDetail { publicint OrderDetailID { get; set; } publicint OrderID { get; set; } publicdecimal Cost { get; set; } publicstring ItemName { get; set; } public Order Order { get; set; } } publicclass MyDomainContext : DbContext { public DbSet<Order> Orders { get; set; } public DbSet<OrderDetail> OrderDetails { get; set; } static MyDomainContext() { Database.SetInitializer<MyDomainContext>( new DropCreateDatabaseIfModelChanges<MyDomainContext>()); } }
上面的示例可以看出,Order类和OrderDetail类没有派生自任何基类,也没有附加EF特性,在将它们添加到上下文(上下文需要派生自DbContext)中时,会自动生成相应的数据表。唯一与EF相关的类MyDomainContext是必须的,它用来提供数据的上下文支持,它可以和Order、OrderDetail类不在同一个应用程序集中。
context 必须满足下面的要求:
- 派生自 System.Data.Entity.DbContext
- 对于你希望使用的每一个实体集定义一个属性
- 每一个属性的类型是 System.Data.Entity.DbSet<T>,T 就是实体的类型
- 每一个属性都是读写属性 read/write ( get/set )
在这里,DbContext 基类通过反射来获取映射到数据库的实体。这遵循一系列的约定。
例如,对于 Order 来说,他的属性 OrderID 必须是主键,其它的约定将用来推断列名和列的类型,默认数据库中的列名是属性名,使用 string 类型来影射数据库中的 nvarchar(128), 如果属性的类型是可空的,那么,影射到数据库中的允许 NULL 等等。以后我们可以看到如果覆盖这些约定。
我们将增加一个静态的构造函数,这个静态的构造函数对于整个应用程序域来说建立一个标准,当数据库的上下文初始化的时候,检查数据库的架构是否与模型相符,如果不是的话,将删除数据库然后重新创建它。EF 将会创建一个名为 dbo.EdmMetadata 的表,然后将模型结构的 Hash 保存到其中来实现。
如果数据库不存在,EF 将会创建它,创建什么数据库呢?默认情况下,将在你的本地机器上,使用上下文对象名称,有许多方式来覆盖这个行为,最简单的方式是在配置文件中增加一个名字为上下文对象名称的数据库连接串,在我这里,叫做 MyDomainContext,还可以通过实现一个构造函数,然后调用非默认的基类构造函数来实现。
—————————————————————————-
覆盖默认约定有两种方式:
- 拦截模型的构建器,使用Fluent API 来修改模型
- 为我们的模型增加标签
通过构建器来覆盖默认约定,我们需要重写 DbContext 的一个方法 OnModelCreating:
protectedoverridevoid OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); //Map schemas modelBuilder.Entity<Order>().ToTable("efdemo.Order"); modelBuilder.Entity<Order>().Property(x => x.OrderID) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity) .IsRequired() .HasColumnName("TheOrderID"); //String columns modelBuilder.Entity<Order>().Property(x => x.OrderTitle) .IsRequired() .HasMaxLength(64); }
这段代码先执行父类的OnModelCreating方法,然后将Order类映射到efdemo架构Order表中,再然后为OrderID设置规则,规定它为标识列,自增,不能为空,且映射到表中的TheOrderID列上面。对于String类型的数据列,还可以指定数据的长度。
使用标注来覆盖默认约定,这种方式需要的代码量比较小,且表现的更加自然:
publicclass Order { publicint OrderID { get; set; } [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] publicint OrderNumber { get; set; } [Required] [StringLength(32, MinimumLength =2)] publicstring OrderTitle { get; set; } [Required] [StringLength(64, MinimumLength =5)] publicstring CustomerName { get; set; } [Required] public DateTime TransactionDate { get; set; } }
在上面的这段代码中,我们强制了OrderNumber为主键列,且为自增;OrderTitle为不能为空且最大长度为32,最小长度为2,尽管我们如此规定,但最小长度是不会被映射到数据表中的,这一点可以理解,最小长度会在数据存储时进行验证,如果小于2将会抛出异常,无法完成保存。
如何在两种覆盖默认约定的方法中进行选择呢?我们的原则是:使用标注来丰富模型的验证规则;使用 OnModelCreated 来完成数据库的约束(主键,自增长,表名,列类型等等)。
—————————————————————————-
关于数据的加载,在默认情况下, EF4.1 仅仅加载查询中涉及的实体,但是它支持两种特性来帮助你控制加载:贪婪加载和延迟加载。
使用贪婪加载方式获取数据集的代码如下:
var orders = from o in context.Orders.Include("OrderDetails").Include("Businesses") where o.CustomerName =="Mac" select o;
这段代码在加载Orders的时候,会将OrderDetails信息和Business信息也加载到orders中。这样的查询会引起效率问题,容易使程序性能变差。鉴于性能问题,EF4.1还支持一种延迟加载的数据加载方式,默认情况下,延迟加载是被支持的,如果你希望禁用它,必须显式声明,最好的位置是在 DbContext 的构造器中:
public MyDomainContext() { this.Configuration.LazyLoadingEnabled =false; }
当禁用了延迟加载以后,当查询一个实体集的时候,相关的子实体也一并加载。当 EF 访问实体的子实体的时候是如何工作的呢?你的集合是 POCO 的集合,所以,在访问的时候没有事件发生,EF 通过从你定义的实体派生一个动态的对象,然后覆盖你的子实体集合访问属性来实现。这就是为什么需要标记你的子实体集合属性为 virtual 的原因。
publicclass Order { publicint OrderID { get; set; } publicstring OrderTitle { get; set; } publicstring CustomerName { get; set; } public DateTime TransactionDate { get; set; } publicvirtual List<OrderDetail> OrderDetails { get; set; } publicvirtual List<Business> Businesses { get; set; } }
贪婪加载:减少数据访问的延迟,在一次数据库的访问中返回所有的数据;你需要知道你将作什么,并且显式声明。延迟加载:非常宽容,因为只在需要的时候加载数据,不需要预先计划;可能因为数据访问的延迟而降低性能,考虑到每访问父实体的子实体时,就需要访问数据库。两种方式各有优缺点,该怎么选择呢?除非需要循环中加载数据,我使用延迟加载。这样的话,可能会造成2-3 次服务器的查询,但是仍然是可以接受的,特别是考虑到贪婪加载的效率问题
—————————————————————————-
默认情况下,EF4.1 将类映射到表,这是约定,但是有时候,我们需要模型比表的粒度更细一些。地址是一个典型的例子,看一下下面的客户类:
publicclass Client { publicint ClientID { get; set; } [Required] [StringLength(32, MinimumLength=2)] publicstring ClientName { get; set; } public Address ResidentialAddress { get; set; } public Address DeliveryAddress { get; set; } } publicclass Address { [Required] publicint StreetNumber { get; set; } [Required] [StringLength(32, MinimumLength=2)] publicstring StreetName { get; set; } }
在上面的例子代码中,Client类的两个Address属性会被映射到表Address中,如果我们希望将Address都映射到一个表中,将地址展开,这需要使用复杂类型,通过构造器来覆盖默认约定,代码如下:
protectedoverridevoid OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<Client>().Property(x => x.ClientID) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); modelBuilder.ComplexType<Address>(); modelBuilder.Entity<Client>().Property(i => i.ResidentialAddress.StreetNumber).HasColumnName("ResStreetNumber"); modelBuilder.Entity<Client>().Property(i => i.ResidentialAddress.StreetName).HasColumnName("ResStreetName"); modelBuilder.Entity<Client>().Property(i => i.DeliveryAddress.StreetNumber).HasColumnName("DelStreetNumber"); modelBuilder.Entity<Client>().Property(i => i.DeliveryAddress.StreetName).HasColumnName("DelStreetName"); }
首先,我指定 client-id 作为自动增长的标识列。然后,指定 Address 是复杂类型。如果愿意的话,也可以将 [ComplexType] 标签加到类上来说明。然后,使用 Lambda 表达式将每一个子属性映射到列上,这将会生成如下的表。模型的使用:
using (var context1 =new MyDomainContext()) { var client =new Client { ClientName ="Joe", ResidentialAddress =new Address { StreetNumber =15, StreetName ="Oxford" }, DeliveryAddress =new Address { StreetNumber =514, StreetName ="Nolif" } }; context1.Clients.Add(client); context1.SaveChanges(); } using (var context2 =new MyDomainContext()) { var clients = from w in context2.Clients where w.ClientName =="Joe" select w; foreach (var client in clients) { Console.WriteLine("client residential StreetNumber: "+ client.ResidentialAddress.StreetNumber); Console.WriteLine("client residential StreetName: "+ client.ResidentialAddress.StreetName); Console.WriteLine("client delivery StreetNumber: "+ client.DeliveryAddress.StreetNumber); Console.WriteLine("client delivery StreetName: "+ client.DeliveryAddress.StreetName); } }
对于复杂类型,最值得注意的是空的管理。即使复杂类型的所有属性都是可空的,你也不能将整个复杂类型的对象设为 null, 例如,在这种情况下,即使街道的名称和街道的号码不是必填的,也不能有一个住宅的地址为 null,需要创建一个所有属性都是 null 的地址对象来表示。同样的道理,当你获取一个实体的时候,即使所有的属性都是 null ,EF4.1 也将会创建一个复杂类型的对象。
—————————————————————————-
在通常的业务环境中,我们需要处理多对多的关系,例如,一个订单都有哪些员工参与,一个员工参与过哪些订单,这就需要在原有的订单类中加入员工的实体列表,并在员工实体中加入订单的实体列表。相应的实体代码如下:
publicclass Order { publicint OrderID { get; set; } [Required] [StringLength(32, MinimumLength =2)] publicstring OrderTitle { get; set; } [Required] [StringLength(64, MinimumLength=5)] publicstring CustomerName { get; set; } public DateTime TransactionDate { get; set; } publicbyte[] TimeStamp { get; set; } publicvirtual List<OrderDetail> OrderDetails { get; set; } publicvirtual List<Employee> InvolvedEmployees { get; set; } } publicclass Employee { publicint EmployeeID { get; set; } publicstring EmployeeName { get; set; } publicvirtual List<Order> Orders { get; set; } }
有了这段代码,EF就会为我们创建一个订单与员工的对应关系表(OrderEmployee),这张表中有两个字段:员工ID(Employee_EmployeeID)与订单ID(Order_OrderID)。这是EF的默认约定,如果要修改关系表的名称,并修改对应的字段的名称,我们可以使用下面的代码来完成:
modelBuilder.Entity<Employee>() .HasMany(e => e.Orders) .WithMany(e => e.InvolvedEmployees) .Map(m => { m.ToTable("EmployeeOrder"); m.MapLeftKey("EmployeeID"); m.MapRightKey("OrderID"); });
通过这段代码,还可以控制没有映射到表的类。下面我们来测试这个模型:
using (var context =new MyDomainContext()) { var order =new Order { OrderTitle ="Pens", CustomerName ="Mcdo’s", TransactionDate = DateTime.Now, InvolvedEmployees =new List<Employee>() }; var employee1 =new Employee { EmployeeName ="Joe", Orders =new List<Order>() }; var employee2 =new Employee { EmployeeName ="Black", Orders =new List<Order>() }; context.Orders.Add(order); order.InvolvedEmployees.Add(employee1); order.InvolvedEmployees.Add(employee2); context.SaveChanges(); }
在这个例子中,我甚至都没有在数据上下文中将雇员加入到雇员的集合中,因为他们被引用到订单的集合中,EF 帮我们完成了。
—————————————————————————-
通常情况下,我们的业务环境需要有并发的处理。对于悲观的并发处理,需要加入记录锁的机制,随之而来带来一些问题,例如,在自动释放锁之前,系统应该锁定多长的时间;乐观并发要简单一些,乐观并发假定用户的修改很少冲突,我们要在记录中加入数据行的版本号,当用户保存记录的时候,通过验证版本号,如果版本号一致,则验证通过,进行保存,如果版本号不一致,则拒绝保存。
在 EF 中,这被称为并发标识 concurrenty token,在这篇文章中,我使用 SQL Server 的 time-stamp 特性,这需要在表中增加一个 time-stamp 类型的列,我们通过它来实现乐观并发。由 SQL Server 在每次记录被更新的时候维护这个列。为了告诉 EF 在实体中有一个属性表示并发标识,你可以通过标签 [ConcurrencyCheck] 来标识这个属性,或者使用模型构建器。我认为并发标识定义了业务规则,应该是模型的一部分。所以这里使用标签。相应的模型代码如下:
publicclass Order { publicint OrderID { get; set; } [Required] [StringLength(32, MinimumLength =2)] publicstring OrderTitle { get; set; } [Required] [StringLength(64, MinimumLength=5)] publicstring CustomerName { get; set; } public DateTime TransactionDate { get; set; } [ConcurrencyCheck] [Timestamp] publicbyte[] TimeStamp { get; set; } publicvirtual List<OrderDetail> OrderDetails { get; set; } publicvirtual List<Employee> InvolvedEmployees { get; set; } }
在这段代码中,当我们通过 DbContext 调用 SaveChanges 的时候,将会使用乐观并发。Timestamp 属性的类型是 byte[], 通过标签 Timestamp ,将这个属性映射到 SQL Server 的 time-stamp 类型的列。
—————————————————————————-
在 ORM 文献中,有三种方式将对象的继承关系映射到表中。
- 每个类型一张表 TPT: 在继承层次中的每个类都分别映射到数据库中的一张表,彼此之间通过外键关联。
- 继承层次中所有的类型一张表 TPH:对于继承层次中的所有类型都映射到一张表中,所有的数据都在这张表中。
- 每种实现类型一张表 TPC: 有点像其他两个的混合,对于每种实现类型映射到一张表,抽象类型像 TPH 一样展开到表中。
这里我将讨论 TPT 和 TPH,EF 的好处是可以混合使用这些方式。
为了模拟实际的业务需求,我定义了一个简单的继承层次,一个抽象基类和两个派生类,代码如下:
publicabstractclass PersonBase { publicint PersonID { get; set; } [Required] publicstring FirstName { get; set; } [Required] publicstring LastName { get; set; } publicint Age { get; set; } } publicclass Worker : PersonBase { publicdecimal AnnualSalary { get; set; } } publicclass Retired : PersonBase { publicdecimal MonthlyPension { get; set; } }
使用TPT方式:我们需要告诉构造器如何创建表:
protectedoverridevoid OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<PersonBase>().HasKey(x => x.PersonID); modelBuilder.Entity<PersonBase>().Property(x => x.PersonID) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); // TPT mapping modelBuilder.Entity<PersonBase>().ToTable("tpt.Person"); modelBuilder.Entity<Worker>().ToTable("tpt.Worker"); modelBuilder.Entity<Retired>().ToTable("tpt.Retired"); }
使用TPH方式:TPH 是 EF 实际上默认支持的。我们可以简单地注释到前面例子中的对表的映射来使用默认的机制。
protectedoverridevoid OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<PersonBase>().HasKey(x => x.PersonID); modelBuilder.Entity<PersonBase>().Property(x => x.PersonID) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); // TPT mapping //modelBuilder.Entity<PersonBase>().ToTable("tpt.Person"); //modelBuilder.Entity<Worker>().ToTable("tpt.Worker"); //modelBuilder.Entity<Retired>().ToTable("tpt.Retired"); }
结果是现在使用一张表来影射整个的继承层次。整个的层次被展开到一张表中,基类中没有的属性被自动标记为可空。还有一个额外的区分列,用来保存数据是属于哪一个类,当 EF 读取一行的时候,区分列被 EF 用来知道应该创建实例的类型,因为现在所有的类都被映射到了一张表中。
混合使用 TPH 和 TPT:我定义了 Worker 的两个子类,我希望将这两个类和 Worker 基类映射到一张表:
publicclass Manager : Worker { publicint? ManagedEmployeesCount { get; set; } } publicclass FreeLancer : Worker { [Required] publicstring IncCompanyName { get; set; } }
注意:每一个属性都必须是可空的。这在 TPH 中非常不方便,现在我们使用模型构建器来完成。
protectedoverridevoid OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<PersonBase>().HasKey(x => x.PersonID); modelBuilder.Entity<PersonBase>().Property(x => x.PersonID) .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); // TPT mapping modelBuilder.Entity<PersonBase>().ToTable("tpt.Person"); modelBuilder.Entity<Retired>().ToTable("tpt.Retired"); // TPH mapping modelBuilder.Entity<Worker>() .Map<FreeLancer>(m => m.Requires(f => f.IncCompanyName).HasValue()) .Map<Manager>(m => m.Requires(ma => ma.ManagedEmployeesCount).HasValue()) .ToTable("tph.Worker"); }
—————————————————————————-
像所有优秀的框架一样,EF 知道它并不能优秀到覆盖所有的角落,通过允许直接访问数据库,EF 支持开放底层的 ADO.NET 框架。EF开放了三个API支持直接查询:
DbContext.Database.ExecuteSqlCommand:这是一个典型的ADO.NET的Command对象,不做解释。
DbContext.Database.SqlQuery:这个方法将返回的数据集映射到相应的对象,而不去管这个对象是不是实体。重要的是 EF 不会跟踪返回的对象,即使他们是真正的实体对象。
DbSet.SqlQuery:这个方法返回的实体将会被 EF 跟踪修改,所以,如果你在这些返回的实体上做了修改,当 DbContext.SaveChanges 被调用的时候,将会被处理。从另一个方面来说,也不能覆盖列的映射。
另外一个 EF 映射管理的方法是使用 Entity SQL,这种方式是 EF 将实体模型转换为物理模型,然后将Linq查询添加到物理模型中,最后将物理模型转换为数据库存储的查询。举例来说,我们可以不在DbContext中定义,而获得我们需要的实体集:
protectedoverridevoid OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<SimpleEntry>().HasEntitySetName("MyEntry"); modelBuilder.Entity<SimpleEntry>().ToTable("MyEntry", "man"); modelBuilder.Entity<SimpleEntry>() .Property(s => s.ID) .HasColumnName("SimpleEntryID"); modelBuilder.Entity<SimpleEntry>() .Property(s => s.Name) .HasColumnName("SimpleEntryName"); }
然后,我们将查询方法暴漏出来:
public IEnumerable<SimpleEntry> GetSimpleEntries() { IObjectContextAdapter adapter =this; var entries = adapter.ObjectContext.CreateQuery<SimpleEntry>("SELECT VALUE MyEntry FROM MyEntry"); return entries; }
这里使用了ObjectContext进行查询,和直接使用Sql进行查询的优势在于,我们可以在 LINQ 之上进行查询,最终进行查询的 SQL 是经过合并的。因此,我们可以通过从一个返回任何结果的简单查询开始,然后在其上应用 LINQ来得到有效的查询,而不需要在使用方查询整个表。
现在,如果你希望能够截获实体的 Insert, Update, 和 Delete 操作,就要靠你自己了。你需要重写 DbContext.SaveChanges ,获取特定状态的实体,实现自己的数据操作逻辑来保存修改,然后在调用 base.SaveChanges 之前将这些实体的状态切换到 Unmodified 。这可以用,但这是一种特殊的技巧。