CodeFirst提供了一种先从代码开始工作,并根据代码直接生成数据库的工作方式。Entity Framework 4.1在你的实体不派生自任何基类、不添加任何特性的时候正常的附加数据库。另外呢,实体的属性也可以添加一些标签,但这些标签不是必须的。下面是一个简单的示例:
- public class Order
- {
- public int OrderID { get; set; }
- public string OrderTitle { get; set; }
- public string CustomerName { get; set; }
- public DateTime TransactionDate { get; set; }
- public List<OrderDetail> OrderDetails { get; set; }
- }
- public class OrderDetail
- {
- public int OrderDetailID { get; set; }
- public int OrderID { get; set; }
- public decimal Cost { get; set; }
- public string ItemName { get; set; }
- public Order Order { get; set; }
- }
- public class 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类不在同一个应用程序集中。
- 派生自 System.Data.Entity.DbContext
- 对于你希望使用的每一个实体集定义一个属性
- 每一个属性的类型是 System.Data.Entity.DbSet<T>,T 就是实体的类型
- 每一个属性都是读写属性 read/write ( get/set )
----------------------------------------------------------------------------
覆盖默认约定有两种方式:
- 拦截模型的构建器,使用流畅的 API 来修改模型
- 为我们的模型增加标签
通过构建器来覆盖默认约定,我们需要重写 DbContext 的一个方法 OnModelCreating:
- protected override void 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类型的数据列,还可以指定数据的长度。
使用标注来覆盖默认约定,这种方式需要的代码量比较小,且表现的更加自然:
- public class Order
- {
- public int OrderID { get; set; }
- [Key]
- [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int OrderNumber { get; set; }
- [Required]
- [StringLength(32, MinimumLength = 2)]
- public string OrderTitle { get; set; }
- [Required]
- [StringLength(64, MinimumLength = 5)]
- public string 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 的原因。
- public class Order
- {
- public int OrderID { get; set; }
- public string OrderTitle { get; set; }
- public string CustomerName { get; set; }
- public DateTime TransactionDate { get; set; }
- public virtual List<OrderDetail> OrderDetails { get; set; }
- public virtual List<Business> Businesses { get; set; }
- }
贪婪加载:减少数据访问的延迟,在一次数据库的访问中返回所有的数据;你需要知道你将作什么,并且显式声明。延迟加载:非常宽容,因为只在需要的时候加载数据,不需要预先计划;可能因为数据访问的延迟而降低性能,考虑到每访问父实体的子实体时,就需要访问数据库。两种方式各有优缺点,该怎么选择呢?除非需要循环中加载数据,我使用延迟加载。这样的话,可能会造成2-3 次服务器的查询,但是仍然是可以接受的,特别是考虑到贪婪加载的效率问题
----------------------------------------------------------------------------
默认情况下,EF4.1 将类映射到表,这是约定,但是有时候,我们需要模型比表的粒度更细一些。地址是一个典型的例子,看一下下面的客户类:
- public class Client
- {
- public int ClientID { get; set; }
- [Required]
- [StringLength(32, MinimumLength=2)]
- public string ClientName { get; set; }
- public Address ResidentialAddress { get; set; }
- public Address DeliveryAddress { get; set; }
- }
- public class Address
- {
- [Required]
- public int StreetNumber { get; set; }
- [Required]
- [StringLength(32, MinimumLength=2)]
- public string StreetName { get; set; }
- }
在上面的例子代码中,Client类的两个Address属性会被映射到表Address中,如果我们希望将Address都映射到一个表中,将地址展开,这需要使用复杂类型,通过构造器来覆盖默认约定,代码如下:
- protected override void 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 也将会创建一个复杂类型的对象。
----------------------------------------------------------------------------
在通常的业务环境中,我们需要处理多对多的关系,例如,一个订单都有哪些员工参与,一个员工参与过哪些订单,这就需要在原有的订单类中加入员工的实体列表,并在员工实体中加入订单的实体列表。相应的实体代码如下:
- public class Order
- {
- public int OrderID { get; set; }
- [Required]
- [StringLength(32, MinimumLength = 2)]
- public string OrderTitle { get; set; }
- [Required]
- [StringLength(64, MinimumLength=5)]
- public string CustomerName { get; set; }
- public DateTime TransactionDate { get; set; }
- public byte[] TimeStamp { get; set; }
- public virtual List<OrderDetail> OrderDetails { get; set; }
- public virtual List<Employee> InvolvedEmployees { get; set; }
- }
- public class Employee
- {
- public int EmployeeID { get; set; }
- public string EmployeeName { get; set; }
- public virtual 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] 来标识这个属性,或者使用模型构建器。我认为并发标识定义了业务规则,应该是模型的一部分。所以这里使用标签。相应的模型代码如下:
- public class Order
- {
- public int OrderID { get; set; }
- [Required]
- [StringLength(32, MinimumLength = 2)]
- public string OrderTitle { get; set; }
- [Required]
- [StringLength(64, MinimumLength=5)]
- public string CustomerName { get; set; }
- public DateTime TransactionDate { get; set; }
- [ConcurrencyCheck]
- [Timestamp]
- public byte[] TimeStamp { get; set; }
- public virtual List<OrderDetail> OrderDetails { get; set; }
- public virtual List<Employee> InvolvedEmployees { get; set; }
- }
在这段代码中,当我们通过 DbContext 调用 SaveChanges 的时候,将会使用乐观并发。Timestamp 属性的类型是 byte[], 通过标签 Timestamp ,将这个属性映射到 SQL Server 的 time-stamp 类型的列。
----------------------------------------------------------------------------
在 ORM 文献中,有三种方式将对象的继承关系映射到表中。
- 每个类型一张表 TPT: 在继承层次中的每个类都分别映射到数据库中的一张表,彼此之间通过外键关联。
- 继承层次中所有的类型一张表 TPH:对于继承层次中的所有类型都映射到一张表中,所有的数据都在这张表中。
- 每种实现类型一张表 TPC: 有点像其他两个的混合,对于每种实现类型映射到一张表,抽象类型像 TPH 一样展开到表中。
这里我将讨论 TPT 和 TPH,EF 的好处是可以混合使用这些方式。
为了模拟实际的业务需求,我定义了一个简单的继承层次,一个抽象基类和两个派生类,代码如下:
- public abstract class PersonBase
- {
- public int PersonID { get; set; }
- [Required]
- public string FirstName { get; set; }
- [Required]
- public string LastName { get; set; }
- public int Age { get; set; }
- }
- public class Worker : PersonBase
- {
- public decimal AnnualSalary { get; set; }
- }
- public class Retired : PersonBase
- {
- public decimal MonthlyPension { get; set; }
- }
使用TPT方式:我们需要告诉构造器如何创建表:
- protected override void 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 实际上默认支持的。我们可以简单地注释到前面例子中的对表的映射来使用默认的机制。
- protected override void 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 基类映射到一张表:
- public class Manager : Worker
- {
- public int? ManagedEmployeesCount { get; set; }
- }
- public class FreeLancer : Worker
- {
- [Required]
- public string IncCompanyName { get; set; }
- }
注意:每一个属性都必须是可空的。这在 TPH 中非常不方便,现在我们使用模型构建器来完成。
- protected override void 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中定义,而获得我们需要的实体集:
- protected override void 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 。这可以用,但这是一种特殊的技巧。