POCO
Entity Framework 4.0为实体提供了简单传统CLR对象(Plain Old CLR Object / POCO)支持。实体对象可以独立于EF存在,由此EF更好地支持了测试驱动开发(test-driven development)和 领域驱动设计(domain-driven design)。同时,EF仍旧可以帮助跟踪POCO实体的变化,允许延迟加载,也会自动修正对导航属性(navigation properties)和外键的改动。
关闭默认代码生成
虽然POCO允许你以透明持久化的方式编写自己的实体类,但还是有必要“接入”持久性和EF元数据,这样你的POCO实体可以从数据库中复原,以及持久化到数据库中。为实现这个,你还是需要使用实体框架设计器创建一个实体数据模型(Entity Data Model),或者提供跟你在Entity Framework 3.5中生成的完全一样的CSDL, SSDL 和 MSL 元数据文件。所以,首先,我将使用ADO.NET实体数据模型向导(Entity Data Model Wizard)来生成一个EDMX。(CodeFirst 模式则不需要此步)
编写POCO实体类
EDMX:
代码如下:(注意红色代码)
public class CustomerType
{
public int CustomerTypeId { get; set; }
public string Description { get; set; }
public ISet<Customer> Customers { get; set; } //导航属性,如果为一对多,则此类型必须继承自“ICollection<T>”
public CustomerType()
{
this.Customers = new HashSet<Customer>();
}
}
public class Customer
{
public int CustomerId { get; set; }
public int CustomerTypeId { get; set; }
public string Name { get; set; }
public CustomerType CustomerType { get; set; }
public ISet<CustomerEmail> CustomerEmails { get; set; }
public Customer()
{
this.CustomerEmails = new HashSet<CustomerEmail>();
}
}
public class CustomerEmail
{
public int CustomerEmailId { get; set; }
public int CustomerId { get; set; }
public string Email { get; set; }
public Customer Customer { get; set; }
}
编写实体框架上下文
为把所有这些东西结合在一起,我所要做的最后一件事情是,提供一个上下文实现(就象使用默认的代码生成时你得到的ObjectContext实现一样)。上下文(context)是把持久化意识带进你的应用的胶水(glue),它允许你编写查询,将实体复原,以及将变化存回到数据库中去。
代码如下:
...
using System.Data.Objects;
...
public class EFCSharpTestEntities : ObjectContext
{
private ObjectSet<CustomerType> _customerTypes;
private ObjectSet<Customer> _customers;
private ObjectSet<CustomerEmail> _customerEmails;
public EFCSharpTestEntities()
: base("name=CSharpTestEntities", "CSharpTestEntities")
{
_customerTypes = CreateObjectSet<CustomerType>();
_customers = CreateObjectSet<Customer>();
_customerEmails = CreateObjectSet<CustomerEmail>();
}
public ObjectSet<CustomerType> CustomerTypes
{
get { return _customerTypes; }
}
public ObjectSet<Customer> Customers
{
get { return _customers; }
}
public ObjectSet<CustomerEmail> CustomerEmails
{
get { return _customerEmails; }
}
}
注:ObjectSet<T> 是Entity Framework 4.0中引进的一个特殊的ObjectQuery<T>。
使用模型 “EFCSharpTestEntities”
代码片断:摘自上篇文章的示例
using (var context = new EFCSharpTestEntities())
{
var web = new CustomerType { Description = "Web Customer", CustomerTypeId = 1 };
var retail = new CustomerType { Description = "Retail Customer", CustomerTypeId = 2 };
//添加客户“Joan Smith”,所属类型"web",拥有两个“Email”。
var customer = new Customer { Name = "Joan Smith", CustomerType = web };
customer.CustomerEmails.Add(new CustomerEmail { Email = "jsmith@gmail.com" });
customer.CustomerEmails.Add(new CustomerEmail { Email = "joan@smith.com" });
context.Customers.AddObject(customer);
customer = new Customer { Name = "Bill Meyers", CustomerType = retail };
customer.CustomerEmails.Add(new CustomerEmail { Email = "bmeyers@gmail.com" });
context.Customers.AddObject(customer);
//提交添加
context.SaveChanges();
}
using (var context = new EFCSharpTestEntities())
{
//EF4默认不开启延迟加载功能,如果略掉Include,则在访问子项时会出错。
var customers = context.Customers.Include("CustomerType").Include("CustomerEmails");
Console.WriteLine("Customers");
Console.WriteLine("=========");
foreach (var customer in customers)
{
Console.WriteLine("{0} is a {1}, email address(es)", customer.Name, customer.CustomerType.Description);
foreach (var email in customer.CustomerEmails)
{
Console.WriteLine("\t{0}", email.Email);
}
}
}
Console.WriteLine("Press <enter> to continue...");
Console.ReadLine();
加载相关 POCO 实体
因为 POCO 实体与自 EntityObject 继承的对象并不具有相同的关系要求,所以加载相关对象所需的过程与上篇文章介绍的将略有不同。
显式加载
因为返回 EntityCollection<(Of <(<'TEntity>)>)> 或 EntityReference<(Of <(<'TEntity>)>)> 类型并不要求 POCO 实体的导航属性,所以,通过使用这些类实现的 Load 方法无法执行相关对象的显式加载。而是必须使用 ObjectContext 类的 LoadProperty 方法显式加载相关对象。(MSDN)
代码片断:
...
using (var context = new EFCSharpTestEntities())
{
Console.WriteLine("Customers");
Console.WriteLine("=========");
foreach (var customer in context.Customers)
{
Console.WriteLine("Name:{0}", customer.Name);
context.LoadProperty(customer, c => c.CustomerEmails);
foreach (var email in customer.CustomerEmails)
{
Console.WriteLine("Email:{0}", email.Email);
}
}
}
Console.WriteLine("Press <enter> to continue...");
Console.ReadLine();
...
运行效果:
注:不写显示加载代码时,则不会执行foreach遍历,也不会出现异常。
延迟加载
若要启用 POCO 实体的延迟加载并且在发生更改时希望实体框架跟踪类中的更改,则 POCO 类必须满足以下所述的要求,以便实体框架可以在运行时期间为 POCO 实体创建代理。代理类派生自 POCO 类型。可以通过将 LazyLoadingEnabled 选项设置为 false 来禁用延迟加载。
对于要创建的任何代理:
· 必须使用公共访问声明自定义数据类。
· 自定义数据类不得为 sealed
· 自定义数据类不得为 abstract。
· 自定义数据类必须具有一个不带参数的 public 或 protected 构造函数。 如果希望使用 CreateObject<(Of <<'(T>)>>) 方法为 POCO 实体创建代理,请使用不带参数的 protected 构造函数。 调用 CreateObject<(Of <<'(T>)>>) 方法不保证会创建该代理:POCO 类必须满足本主题中所述的其他要求。
· 该类无法实现 IEntityWithChangeTracker 或 IEntityWithRelationships 接口,因为代理类实现这些接口。
· 必须将 ProxyCreationEnabled 选项设置为 true。
对于延迟加载代理:
· 必须将每个导航属性声明为 public、virtual,而不是 sealed get 访问器。 自定义数据类中定义的导航属性必须在概念模型中具有一个相应的导航属性。
对于更改跟踪代理:
· 映射到数据模型中实体类型属性的每个属性必须具有非密封、public 和 virtual get 和 set 访问器。
· 表示关系“多”端的导航属性必须返回实现 ICollection<(Of <(<'T>)>)> 的类型,其中 T 是该关系另一端处对象的类型。
· 如果希望代理类型随对象一起创建,请在创建新对象时使用 ObjectContext 的 CreateObject<(Of <<'(T>)>>) 方法,而不是使用 new 运算符。
代码片断:
using (var context = new EFCSharpTestEntities())
{
context.ContextOptions.LazyLoadingEnabled = true;
Console.WriteLine("Customers");
Console.WriteLine("=========");
foreach (var customer in context.Customers)
{
Console.WriteLine("Name:{0}", customer.Name);
//context.LoadProperty(customer, c => c.CustomerEmails);
foreach (var email in customer.CustomerEmails)
{
Console.WriteLine("Email:{0}", email.Email);
}
}
}
...
public class Customer
{
public int CustomerId { get; set; }
public int CustomerTypeId { get; set; }
public string Name { get; set; }
public virtual CustomerType CustomerType { get; set; }
public virtual ISet<CustomerEmail> CustomerEmails { get; set; }
public Customer()
{
this.CustomerEmails = new HashSet<CustomerEmail>();
}
}
...
原理:将导航属性标记为virtual后,这允许实体框架在运行时为 POCO 类型提供一个代理(proxy)实例,正是这个代理实现了自动的延迟装载。该代理实例是基于一个继承自我的POCO实体类的类型,所以提供的所有功能都被保留下来了。从开发人员的角度来看,即使延迟装载或许是个需求,这也允许你编写透明持久性的代码。如果你在调试器中检查实际的实例时,你会看到该实例的底层类型与我原先声明的类型是不同的。
预先加载
可以指定查询路径来返回相关的 POCO 实体。使用 Include 方法可以预先返回相关对象,就像对待工具生成的实体类型一样。
如:var customers = context.Customers.Include("CustomerType").Include("CustomerEmails");
关于预先加载可以参数上篇文章。
复杂类型(Complex Types)
POCO 中的复杂类型支持跟常规的基于EntityObject的实体中的复杂类型支持一样。你要做的就是将它们声明为POCO类,然后在你的POCO实体中使用和声明基于它们的属性。
代码片断:
...
public class Employee
{
public int EmployeeId { get; set; }
public string Email { get; set; }
public Name Name { get; set; }
}
public class Name
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
...
using (var context = new EFCSharpTestEntities())
{
context.Employee.AddObject(new Employee {
Name = new Name { FirstName="gu",LastName="hongxing"} ,
Email="passvcword@126.com"});
context.Employee.AddObject(new Employee
{
Name = new Name { FirstName = "A", LastName = "Star" },
Email = "77090302@qq.com"
});
context.SaveChanges();
foreach (var employee in context.Employee.OrderBy(e => e.Name.LastName))
{
Console.WriteLine("{0}, {1} email: {2}",
employee.Name.LastName,
employee.Name.FirstName,
employee.Email);
}
}
...
注意:
· 必须将复杂类型定义为类(class),结构体(struct)是不支持的。
· 在你的复杂类型类中,你不能使用继承。
· VS2010运行在图形化创建复杂类型。
基于快照的变动跟踪(不用代理的纯POCO类)
基于快照(Snapshot)的变动跟踪,就是纯粹的POCO实体的做法,不使用代理来处理变动跟踪。这是个简明的变动跟踪方案,依赖于实体框架所维护的之前和之后值的完整快照,在SaveChanges中对这些值进行比较,决定到底哪些值与初始值有所不同。在这个模型中,除非你用了懒式装载,你的实体的运行时类型跟你定义的POCO实体类型是一模一样的。
这个做法没什么问题,如果你在乎你的实体类型的运行时纯粹性(不使用代理),完全可以依赖这个方案,走纯粹的POCO实体之路(不依赖于代理类型实现额外的功能)。
基于快照的变动跟踪唯一潜在的问题是,有几件事情你需要注意,因为在你的对象变动时没有向实体框架做直接变动通知,实体框架的对象状态管理器将与你的对象图不再同步。
代码片断:
Customer customer = (from c in context.Customers
where c.Name == "Joan Smith"
select c).Single();
ObjectStateEntry ose = context.ObjectStateManager.GetObjectStateEntry(customer);
Console.WriteLine("Customer object state: {0}", ose.State);
customer.Name = "Astar";
Console.WriteLine("Customer object state: {0}", ose.State);
运行结果:
在这个例子中,Customer是个纯POCO类型,对该实体做变动并不自动与状态管理器保持同步,因为在纯POCO实体与实体框架间没有自动的通知机制。所以,在查询状态管理器时,它会认为你的客户对象的状态是Unchanged(未被改动),尽管我们显式地对该实体的一个属性做了变动。
如果调用SaveChanges而不选择acceptChangesDuringSave(在保存后接受变动之选项)的话,你会看到在保存后,其状态变成了Modified(改动过了)。这是因为在保存时,基于快照的变动跟踪机制开始起作用,检测到了变动。当然,默认的SaveChanges调用会将状态变回到 Unchanged ,因为默认的Save行为是在保存后接受变动。
使用代理的基于通知的变动跟踪
如果你在乎在你对实体值,关系和对象图做变动时的非常高效的和即时性的变动跟踪的话,这是个另样的方案,基于代理的变动跟踪。如果你把某个特定实体类型上的所有映射属性都声明为virtual的话,你就可以利用基于代理的变动跟踪了。
使用代理来跟踪变动的实体总是与实体框架的对象状态管理器保持同步,因为代理会在实体的值和关系变动时通知实体框架。总的来说,这使得变动跟踪更为有效,因为对象状态管理器可以略去比较属性的原始值和当前值这一步,如果它知道属性没有变动的话。
实际上,你从代理上得到的变动跟踪行为跟从基于EntityObject的非POCO实体或IPOCO实体上得到的变动跟踪行为是完全一样的。在你对实体做变动时,对象状态管理器被通知到你的变动了。在调用SaveChanges时,不会导致另外的花销(overhead)。
但是,基于代理的变动跟踪也意味着你的实体的运行时类型跟你定义的类型不完全一样,而是你的类型的子类。这在许多场景下(譬如序列化)也许不太合适,你需要选择在你的应用和领域的需求和约束下最合适的方法。
不使用代理,与状态管理器保持同步
使用基于快照的纯POCO实体时,有一个至关重要的方法:ObjectContext.DetectChanges()。这个API在你无论何时改变对象图时都应该显式调用,它会告知状态管理器它需要与的你对象图做同步。
ObjectContext.SaveChanges 在默认情形下会隐式调用DetectChanges,所以,如果你所做的就是对你的对象们做一系列的变动,然后立刻调用Save的话,你不必显式调用DetectChanges。但是,要记住的是,取决于你的对象图的大小,DetectChanges也许会花销很大(而且,取决于你在做什么,DetectChanges也许是多余的),所以,有可能你可以略去SaveChanges中隐式调用的DetectChanges,这一点,在我们讨论Entity Framework 4.0中新引进的SaveChanges的重载方法时会做更多的讨论。
代码片断:
Customer customer = (from c in context.Customers
where c.Name == "Astar"
select c).Single();
ObjectStateEntry ose = context.ObjectStateManager.GetObjectStateEntry(customer);
Console.WriteLine("Customer object state: {0}", ose.State);
customer.Name = "Joan Smith";
context.DetectChanges();
Console.WriteLine("Customer object state: {0}", ose.State);
运行结果:
显式调用DetectChanges 会导致状态管理器与你的对象的状态保持一致。因为DetectChanges的潜在花销,依赖于状态管理器与对象图保持一致的 ObjectContext上的其他一些APIs是不显式调用它的。因此,无论什么时候你在context上做依赖于状态的操作时,你需要调用DetectChanges。
其它相关问题可参考:http://www.cnblogs.com/lfzx_1227/archive/2009/08/15/1550628.html
常见问题
在使用POCO之前需要一个实体数据模型么?
是的,Entity Framework 4.0中的POCO支持只是去除了在你的实体类中带特定于持久性的关注的需求而已。但还是需要你提供CSDL/SSDL/MSL (总称EDMX)元数据,这样实体框架才能够将你的实体和元数据结合起来,以允许访问数据。
使用POCO实体时,元数据是怎么映射的?
在Entity Framework 3.5中,基于EntityObject和IPOCO的实体都是依赖着使用映射特性(attributes),对实体类型和属性进行修饰和映射到概念性模型中对应的元素的。Entity Framework 4.0 引入了基于约定(convention)的映射,以允许不用显式的修饰,就可将实体类型,属性,复杂类型和关系映射到概念性模型。一个简单的规则是,在你的POCO类中使用的实体类型名称,属性名称,和复杂类型名称必须匹配那些在概念性模型中定义了的相应名称。命名空间的名称不在考虑之中,类中的命名空间定义和概念性模型中的命名空间定义不必相符。
实体类中的所有属性都需要有公开的getters和setters么?
你可以在你的POCO类型的属性上使用任何访问控制修饰符(access modifier),只要被映射的任何属性都不是虚拟的,以及你不需要局部信任(partial trust)支持。在局部信任下运行时,对你的实体类的访问控制修饰符的可见性有一些特定的要求。我们将对在涉及局部信任时,所支持的完整的访问控制修饰符集提供详细的文档。
对基于集合的导航属性都支持哪些集合类型?
任何属于ICollection<T>的类型都是支持的。如果你对ICollection<T>类型的字段不以具体的类型初始化的话,那么从数据库中复原实体集合时, 将提供List<T>。
可以有单向的关系么?例如,在Product类中有一个 Category 属性,但在Category类中我不想要一个Products集合。
是的,这是支持的。唯一的限制是,实体类型必须反映模型中所定义的东西。如果你不想拥有对应于关系的某一边的导航属性的话,那么你需要从模型中将其完全剔除。
POCO支持延迟(懒式)装载么?
是的,POCO是通过使用代理类型来支持延迟(懒式)装载的,这些代理类型是在你的POCO类之上提供自动的懒式装载行为的。
总体注意:
· 继承的 ObjectContext 的构造函数的参数其实就是指定数据库连接串 Connection String
· 工具生成的Edmx的 Connection String 的只保存在该程序集的 app.config 中,记得拷贝到相关的app.config或者web.config。
· Edmx中的Model上的Table Name和Column Name务必和 POCO的名称一致。Entity Framework 4.0 引入了基于约定(convention)的映射,以允许不用显式的修饰,就可将实体类型,属性,复杂类型和关系映射到概念性模型。一个简单的规则是,在你的POCO类中使用的实体类型名称,属性名称,和复杂类型名称必须匹配那些在概念性模型中定义了的相应名称。
· 延迟加载的属性要设置成Virtual, ObjectContext上需要设置 ContextOptions.LazyLoadingEnabled = true 。