{
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 )
----------------------------------------------------------------------------
覆盖默认约定有两种方式:
- 拦截模型的构建器,使用Fluent API 来修改模型
- 为我们的模型增加标签
通过构建器来覆盖默认约定,我们需要重写 DbContext 的一个方法 OnModelCreating:
{
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 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 仅仅加载查询中涉及的实体,但是它支持两种特性来帮助你控制加载:贪婪加载和延迟加载。
使用贪婪加载方式获取数据集的代码如下:
where o.CustomerName == " Mac "
select o;
这段代码在加载Orders的时候,会将OrderDetails信息和Business信息也加载到orders中。这样的查询会引起效率问题,容易使程序性能变差。鉴于性能问题,EF4.1还支持一种延迟加载的数据加载方式,默认情况下,延迟加载是被支持的,如果你希望禁用它,必须显式声明,最好的位置是在 DbContext 的构造器中:
{
this .Configuration.LazyLoadingEnabled = false ;
}
当禁用了延迟加载以后,当查询一个实体集的时候,相关的子实体也一并加载。当 EF 访问实体的子实体的时候是如何工作的呢?你的集合是 POCO 的集合,所以,在访问的时候没有事件发生,EF 通过从你定义的实体派生一个动态的对象,然后覆盖你的子实体集合访问属性来实现。这就是为什么需要标记你的子实体集合属性为 virtual 的原因。
{
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 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都映射到一个表中,将地址展开,这需要使用复杂类型,通过构造器来覆盖默认约定,代码如下:
{
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 表达式将每一个子属性映射到列上,这将会生成如下的表。模型的使用:
{
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 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的默认约定,如果要修改关系表的名称,并修改对应的字段的名称,我们可以使用下面的代码来完成:
.HasMany(e => e.Orders)
.WithMany(e => e.InvolvedEmployees)
.Map(m =>
{
m.ToTable( " EmployeeOrder " );
m.MapLeftKey( " EmployeeID " );
m.MapRightKey( " OrderID " );
});
通过这段代码,还可以控制没有映射到表的类。下面我们来测试这个模型:
{
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 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 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方式:我们需要告诉构造器如何创建表:
{
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 实际上默认支持的。我们可以简单地注释到前面例子中的对表的映射来使用默认的机制。
{
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 int ? ManagedEmployeesCount { get ; set ; }
}
public class FreeLancer : Worker
{
[Required]
public string IncCompanyName { get ; set ; }
}
注意:每一个属性都必须是可空的。这在 TPH 中非常不方便,现在我们使用模型构建器来完成。
{
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中定义,而获得我们需要的实体集:
{
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 " );
}
然后,我们将查询方法暴漏出来:
{
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 。这可以用,但这是一种特殊的技巧。