日期:2014-05-20  浏览次数:20940 次

Entity Framework 学习总结之十一:POCO

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