Entity Framework 4 in Action读书笔记——第六章:理解实体的生命周期(三)

objectstatemanager更改跟踪管理

ObjectStateManager组件(从现在开始称之为 state manager)负责与上下中对象追踪有关的一切:

1.当添加,附加到上下文或者从上下文中删除一个实体,实际上是对state manager做的这些。
2.当我们说上下文保留从数据库中读取的所有实体集合在内存中时,其实是state manager保存这些数据。
3.当上下文执行一个身份地图(identity-map)检查,其实是state manager执行的检查。
4.当我们说上下文跟踪实体间关系式,其实是state manager在跟踪。

跟踪实体改变仅仅是state manager的任务之一。它还提供检索实体状态和操作它的API。

state manager不是直接访问的。因为它是上下文的内部组件,它以ObjectContext类的属性公开,叫ObjectStateManager。下面的代码访问state manager的代码:

var osm = ctx.ObjectStateManager;

上下文负责state manager的生命周期,它处理它的初始化和释放。

现在已经知道了state manager的目的,让我们深入看看它如何完成任务。

ObjectStateEntry类

当查询state manager来检索由上下文跟踪的实体,它由ObjectStateEntry(从现在开始称为entry)对象应答。它公开了两种类型的成员:属性和方法。

成员

描述

Entity属性 state manager跟踪的实体
EntityKey 属性 实体的Key
EntitySet属性 实体属于的实体集
EntityState属性 实体的状态
OriginalValues属性 当每个实体附加时的值
CurrentValues属性 每个实体的当前值
GetModifiedProperties方法 从实体被跟踪修改的属性
IsRelationship属性 指定entry是否包含有关实体或关系的数据

最重要的成员是EntityState,OriginalValues和CurrentValues。注意OriginalValuesh和CurrentValues是DbDataRecord类型的。

ObjectStateEntry是抽象类,作为EntityEntry和RelationshipEntry的基类。它们都是内部类,所以不能直接操作它们。根据它们的名字,EntityEntry包含关于实体的数据,RelationshipEntry包含关于实体间关系的信息。

EntityKey属性很重要,因为它表示state manager内实体的键(key)。

理解state manager的key是如何标识对象的?

EntityKey属性是state manager用来确保即有一个给定类型和ID的实体被跟踪。身份地图(identity-map)检查是检查实体的EntityKey属性而不是实体的键属性。EntityKey包含两个重要的属性:实体集和组合成实体主键的属性的值。

当添加一个对象到上下文,就使用临时实体键添加对象到state manager,因为EF知道它必须持久化对象为一个新行。这个临时键没有经过身份地图检查评估,所以如果再添加另一个相同类型和ID的对象,它会使用另一个临时键添加到state manager。当持久化时,就会执行两个INSERT命令。

如果行的ID是由数据库自动生成的,持久化没有问题,如果使用自然键,持久化就会抛出一个duplicate-key的异常,因为第二个INSERT命令使用相同的ID,在数据库中导致主键冲突。

当附加实体时,state manager自动创建一个EntityKey对象并保存在entry(ObjectStateEntry)中。这个EntityKey对象不是临时的,它由身份地图检查(identity-map check)使用。

ObjectStateEntry不仅包含数据,它还合并行为。它允许改变实体的状态以及重写原值和当前值。得到ObjectStateEntry实例的唯一方式是查询state manager。

检索entry

已经清楚的了解了添加,附加和删除实体,为什么还需要为了实体状态查询上下文?有两种情况非常有用:第一,EF本身需要查询对象状态;第二,你可能需要在一些通用日志记录或其他场景中报告实体状态。

假设你想记录每一个由应用程序触发的持久化操作。一种方式可能是创建一个执行附加、添加或者删除并且添加一个entry到记录存储的扩展方法。这种方法的实现可能某些原因需要中止持久化过程并且结束还没有发生的记录操作。
另一种方法是订阅SavingChanges事件,它在持久化过程开始前(SaveChagnes)触发,在Added,Modified和Deleted状态中检索实体并且在日志中写入entry。这个解决方法如下面的清单所示:

QQ截图20111107170123

第一步是挂钩SavingChagnes时间。然后,在处理程序中,使用ObjectStateManager类的GetObjectEntries方法检索特定状态的所有entry。它接受一个EntityState参数要查找的状态,返回一个特定状态所有entry的集合。如果不同状态的entry,可以使用标志语法组合它们。做种,调用logger方法写入entry。

通常,只需要检索单个entry。GetObjectStateEntries在这种情况下不可用。你需要另一个方法,允许传递一个实体,得到相对应的状态管理器(state-manager)的entry。state manager有这样一个方法。

检索单个entry

检索单个实体的entry,可以使用GetObjectStateEntry方法,传递实体作为参数,如下所示:

var entry = osm.GetObjectStateEntry(entity);

输入实体必须有key属性集,因为当state manager尝试检索entry,它使用它们创建一个EntityKey执行查找。如果entry不包含这个EntityKey,方法就会抛出一个InvalidOperationException异常,附带一条信息:The ObjectStateManager does not contain an ObjectStateEntry with a reference to an object of type ‘type’。

为了避免这个异常,可以使用TryGetObjectStateEntry。它和GetObjectStateEntry执行相同的任务;但是遵循了.NET Framework的设计指南,这个方法接收一个实体和一个表示entry找到的输出参数,它返回一个布尔值指定entry是否找到。如果返回false,输出参数为null。看下面的清单:

ObjectStateEntry entry;
var found = osm.TryGetObjectStateEntry(c, out entry);
使用ObjectStateEntry类,可以使用ChangeState修改实体的状态,如前面所见。但那不是唯一的选择。下面讨论其他允许修改实体状态的方法。

由entry修改实体状态

当有了entry,就可以修改相关实体的状态,因为上下文方法在内部调用ObjectStateEntry类的方法。这些方法如下表所示:

方法

描述

Delete 标记实体为deleted。当移动到Deletted状态时这个方法也由DeleteObject和ChangeState调用。
SetModified 标记实体以及它的所有属性为Modified。当移动到Modified时这个方法在内部由ChangeState调用。
SetModifiedProperty 标记一个属性为Modified,因此也标记实体。
AcceptChanges 改变实体的状态为Unchanged并使用当前值重写entry的原值。
ChangeState 改变实体的状态到输入值。

这些方法使用很简单,因为它们中的大多数都没有参数。只有SetModifiedProperty接收属性的名称和ChangeState接收实体新的状态。

前面提到由state manager自动执行的唯一状态改变是从Unchanged到Modified,但它并不总是这样。下面,深入对象跟踪机制。

理解对象跟踪

从技术上来说,state manager不能监视实体内属性的修改;当修改发生时,实体通知state manager。这种通知机制并不总是起作用——它依赖于你如何初始化实体。可以创建下面类型的实体:

1.没有被代理包装POCO实体(普通实体)
2.由代理包装的实体(代理实体)
包装的实体是一个类,它通过代理启用扩展性。当类的继承不是封闭的和它的属性是virtual时,它就包装的。尤其是如果所有的标量属性都是virtual,包装类启用更改追踪。包装(或代理)实体是已经包装到虚拟代理(virtual proxy)中的实体的实例。

state manager 并不关心类包装与否。重要的是实体是作为代理或者POCO类被实例化。下面我们看一些例子说明它们的区别。

实体的更改追踪没有包装在代理中

实体可能从web服务,web页面的ASP.NET ViewState的反序列化,上下文代理创建禁用的查询,构造函数初始化获得。这些对象没有被代理包装,因为只有启用代理创建的上下文可以创建包装的实体。此外,一个实体可能不是包装的,所以即使它来自上下文,也可能不是代理的。

如第5章中所见,实体的属性setter器不知道state manager,那么state manager是如何知道属性什么时候被修改的呢?你也许会惊讶于它不能。

我们看个例子。假设你需要修改一个customer。你查询数据库检索customer并修改属性,如name,然后持久化它。因为state manager不知道你已经修改了属性,实体的状态仍然保持在Unchanged,如下所示:

var customer = ctx.Customers.First();
var entry = osm.GetObjectStateEntry(customer);
customer.Name = "NewCustomer";                  //State Unchanged
ctx.SaveChanges();

当SaveChanges方法被调用,即使状态是Unchanged,修改也被持久化到数据库。这是怎么做到的呢?为什么state manager不知道的情况下修改被持久化了呢?

神奇之处在于ObjectStateManager类的DetectChanges方法,它在内部由SaveChanges方法调用。这个方法遍历所有的状态管理器(state-manager)entry,并且比较每一个的原值和存储在实体中的值。当它发现属性被修改——在本例中,是customer的name——它标记属性为Modified,进而标记实体为Modified,并且更新entry的当前值。当DetectChanges完成它的任务,state manager中的实体和它们的entry完美的同步,SaveChanges可以继续持久化了。

由于state manager并不会与实体自动同步,无论合适使用它的API,你必须调用DetectChanges方法避免检索过期数据,如前面的清单。看下面的清单:

var entry = osm.GetObjectStateEntry(customer);
customer.Name = "NewCustomer";                      // State Unchanged
ctx.DetectChanges();                               //State Modified

DetectChanges并不是没有问题。它遍历所有的实体并检查它们所有的属性。如果许多实体被跟踪,遍历可能非常浪费。使用它,但不滥用。

更改追踪包装在代理中

当实体被包装在代理中,它下面有更多的神奇。代理实体使自动更改跟踪成为可能,意味着当属性变化时它能及时通知state manager。这是因为代理重写属性setter器,注入代码通知state manager属性发生了改变。下图包含了一个代理内部简单的代码版本。

QQ截图20111107194657

这个功能很棒,不用费劲就实现了state manager和实体的自动同步。看下面的代码:

var entry = osm.GetObjectStateEntry(customer);       //State Unchanged
customer.Name = "NewCustomer";                       //State Modified

在第三章中已经了解到代理启用延迟加载。在本章,已经了解到代理还可以启用自动更改追踪。在EF1.0,这些功能需要一大堆代码,现在好了,实现它们只需一点点代码。

上下文不仅能追踪单个实体,它还可以追踪实体的关系。你可能以为这是使用关联对象的主键属性或外键实现的;有时它是这种方式,有时候又不是。

理解关系跟踪

当实体附加到上下文,一个新的entry被添加到state manager。然后,上下文扫描导航属性查找关联实体。不为null的实体自动附加。当添加实体时也是这样。

当关联实体被附加,如果关系是通过独立关联,一个新的RelationshipEntry被添加到state manager,包含关联实体的相关信息。例如,如果你附加一个order,它有一个对customer和多个order detail的引用,state manager包含order,它的detail,它的customer实体和它们的关联entry。下图显示了附加过程后的state manager。

image

如果order使用查询加载,会有一点点不同。state manager为order,customer和它们的关联各创建一个entry。(忽略order detail,因为集合关联被忽略)。即使不检索带有order的customer也是一样。customer entry值包含主键(在Order表中为CustomerId),然而关系指向两个实体,所以state manager有它需要的一切。

关系可以处于Added或者Deleted状态,但是不能处于Modified状态。通常,不需要修改关系状态,因为它由state manager处理。罕见情况下,需要修改关系状态时,可以使用ObjectStateManager类的ChangeRelationshipState方法或者ObjectStateEntry类的ChangeState方法。当然,如果你尝试修改关系状态为Modified,会得到运行时异常。

如果使用了外键关联,就不会创建关系entry,因为不需要关联实体,仅仅是外键属性。结果是与以前一样附加order后,state manager看起来如下图:

image

当实体是从数据库中检索的,在state manager中实体entry和关系entry都不会被创建。此外,改变关系是没有价值的,因为你仅仅需要改变外键属性。如你所见,外键关联是事情变得简单,减少了state manager的工作。

现在已经知道了state manager是如何跟踪实体和关系的,让我们研究几个注意事项。

只有实体被上下文跟踪,改变才会被跟踪

当实体在上下文范围之外,对它们的改变不能跟踪。如果创建一个order,添加一个detail,或者改变它关联的customer,然后附加order到上下文,上下文永远都不会知道发生了什么。order和关系entry附加时处于Unchanged状态。

state manager不支持部分加载图像

当附加一个实体,上下文扫描所有的导航属性,同时附加相关的对象。(添加一个对象到上下文也是如此)如果它们被附加,所有的实体都处于Unchanged状态,如果它们被添加,则处于Added状态。

如果上下文已经跟踪了与关系图中的实体具有一样的类型和键值的实体,则会引发一个InvalidOperationException异常,因为它不能保存有同键值同类型的两个对象。

当添加一个关系图,没有异常的风险,因为实体键合对象的关联是临时的。如果实体以后标记为Unchanged会引发问题。在这种情况下,EntityKey再生并且变得永久,如果已经存在了相同键的实体,会抛出一个InvalidOperationException异常,附带信息:AcceptChanges cannot continue because the object’s key values conflict with another object in the ObjectStateManager. Make sure that the key values are unique before calling AcceptChanges。

在单引用(single-reference)属性中如何更改关系

假设必须更改关联有order的customer。在两种情况下可以找到自己:

1.Customer已经附加到上下文。如果外键关联起作用,属性是同步的,Order对象变成Modified。如果使用独立关联,实体间只有RelationshipEntry被创建。

2.Customer没有附加到上下文。Customer在Added状态被添加到上下文(记住它不支持部分关系图)。如果关联使用外键关联保持,属性是同步的并且Order对象变成Modified。如果使用独立关联,实体间只有RelationshipEntry被创建。

假设有一个没有客户的订单。如果使用外键关联,设置外键属性为null使order和customer的关联消失。如果使用独立关联,设置Customer属性为null,也导致同样的结果(RelationshipEntry变成Deleted)。

记住只有customer和order之间的关联被移除。没有对象被删除。

在集合属性中如何更改关系

集合属性调用Remove方法导致对master的引用从detail上移除。例如,当从一个order上移除一个detail,它的Order属性被设置为null,它的状态被设置为Modified。因为外键属性(OrderId)不为空,在持久化时,会出现InvalidOperationException异常,附带有一条信息:The operation failed: The relationship
could not be changed because one or more of the foreign-key properties is nonnullable。当更改关系时,关联的外键属性被设置为null值。如果外键不支持null值,一个新的关系必须定义,外键属性必须分配到另一个不为空的值,或者不关联的对象必须删除。

虽然这看起来像加密了一样,其实很清晰。detail的OrderId属性不能为null,因为你不能有独立的order detail。detail必须分配到一个order。如果支持独立的detail,在OrderDetail表的OrderId列则是可空的。同样,OrderDetail类的OrderID属性将使可空的。如果这样,持久化不会抛出任何异常,所以order会被持久化,order detail变成独立的(当然,独立的detail是没有意义的)。

如果使用独立关联,会得到不同的消息:A relationship from the ‘OrderOrderDetail’AssociationSet is in the ‘Deleted’ state. Given multiplicity constraints, a corresponding ‘OrderDetail’ must also in the ‘Deleted’ state。意思是,state manager中的RelationshipEntry被删除了,但是实体是Modified,这是不允许的,因为独立的order detail是不允许的,detail也必须被删除。

这个问题的解决方案是调用上下文的DeleteObject方法代替集合属性的Remove方法。

你可能会对EF为什么不自动删除实体而是简单的移除引用产生疑问。答案是在其他情况下这是不正确的行为。想想supplier和product的多对多关系。如果是那样,如果移除由supplier卖的product,你不必删除它。你只需删除Product-Supplier表的引用。由于这些不同的行为,EF团队谨慎地决定让你显示选择怎么做。

当添加一个实体到集合属性,你可以在两种不同的情况下找到自己,依赖于实体是否附加到上下文:

1.detail被附加到上下文。如果外键关联起作用,属性必须与order的ID同步。如果使用独立关联,只需在实体间创建RelationishipEntry。

2.detail没有附加到上下文。Customer在Added状态(记住,上下文不支持部分关系图)被添加到上下文。如果关联使用外键关联保持,属性必须与order的ID保持同步。如果使用独立关联,实体间也创建RelationshipEntry。

有很多的规则,提前了解它们可以使操作关系图简单点。

更改跟踪和MergeOption

MergeOption是ObjectSet<T>类的一个属性。它是System.Data.Objects.MergeOption类型的枚举,包含下列值:

1.AppendOnly
2.NoTracking
3.OverwriteChanges 
4.PreserveChanges

在对象的具体化期间,当使用AppendOnly(默认设置),state manager检查是否已经存在了相同key的entry。如果是,返回与entry相关的实体和放弃来自数据库的数据。如果没有,实体被具体化并附加到上下文。这种情况下,state manager使用具体化的原值和当前值创建entry。最后,返回具体化的实体。

当使用NoTracking是,上下文不执行身份地图检查,所以来自数据库的数据总是具体化和返回,即使在state manager中已经有了相对应的实体。当NoTracking启用时,返回的实体处于Detached状态,所以上下文不跟踪它们。

当使用OverwriteChanges使用时,如果身份地图检查在state manager没有找到entry,就具体化实体,附加到上下文并返回。如果entry找到了,相关的实体状态设置为Unchanged,当前值和原值使用来自数据库中的值更新。

当使用PreserveChanges是,如果在state manager中身份地图检查没有找到entry,就具体化实体,附加到上下文并返回。如果找到了,有以下发生的可能性:

如果实体的状态是Uchanged,entry中的当前值和原值由数据库的值重写。实体的状态仍保持为Unchanged。
如果实体的状态是Modified,修改的属性的当前值不能被数据库的值重写。没有修改的属性的原值由数据库的值重写。
如果没有修改的属性的当前值不同于来自数据库的值,属性标记为Modified。从1.0版本这是一个重大的改变,因为在那个版本的属性不标记为Modified。如果需要恢复1.0的行为,设置UseLegacyPreserveChangesBehavior属性为true即可,如下:

ctx.ContextOptions.UseLegacyPreserveChangesBehavior = true;

现在已经了解了MergeOption行为。任何应用程序中,它都是重要的一方面,它也经常会被滥用或者被轻视。在第19章会发现它显著的影响了性能。

到此为止,第六章就结束了,下一章学习持久化对象到数据库。

作者:BobTian
出处http://nianming.cnblogs.com/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
欢迎访问我的个人博客:程序旅途
原文地址:https://www.cnblogs.com/nianming/p/2240378.html