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

(一)

我们先从分析实体的生命周期和它的状态开始。

实体生命周期

在其生存期期间,一个实体只有一个状态。在了解如何检索状态之前,先看看什么是实体状态。实体状态就是声明为以下值的System.Data.EntityState类型的枚举:

Added——实体标记为added。
Deleted——实体标记为deleted。
Modified——实体已经被修改。
Unchanged——实体还没有被修改。
Detached——实体不能被追踪。

这些状态代表什么?状态与什么相关?实体如何从一个状态传递到另一个状态?这些状态影响数据库吗?回答这些问题,必须先看看对象生命周期背后的概念。

理解实体状态

如第三章所述,上下文(context)持有对从数据库检索的所有对象的引用。对我们的讨论更重要的是,上下文保持实体的状态并且维护对实体属性的修改。这一功能称为change tracking(或object tracking)。

如果在上下文外部创建一个实体,它的状态为Detached,因为上下文不能追踪它。

如果将一个实体附加到上下文,它的状态就变为Unchanged。如果从数据库中检索一个实体,然后从上下文中移除,实体的状态为Detached。如果检索一个实体,释放上下文,然后创建一个新的上下文,给它添加一个实体,这个实体的状态则为Added。这些例子说明,状态是实体和对它持有引用的上下文间的关系。

让我们看个例子。假设在OrderIT中有两个方法,第一个是检索关于customer的数据,第二个用于更新数据。客户使用第一个方法检索数据并在窗体中显示。在这个方法中,创建了一个上下文来检索数据,然后销毁上下文。

用户修改了一些数据,例如发货地址(shipping address),然后调用第二个方法更新修改的customer数据并保存它。在网络服务(web service)方法中,创建一个新的上下文并且将实体附加到它。新的上下文并不知道什么数据已经被修改了,除非它去数据库中比较。

去数据库中的代价比较大,所以并不会自动执行。这就是说实体附加到上下文时,它进入Unchanged状态,因为上下文对修改一无所知。如果实体状态反映数据库的实际状态,它会是Modified,但是事实并非如此。这个例子很简单,但是它解释了为什么状态是表示实体和上下文间的关系而不是实体和数据库的关系。

当然,实体的状态影响它的持久化方式。这并不奇怪,当持久化实体时,在数据库中使用INSERT, UPDATE, 或DELETE命令保存那些AddedModified或者Deleted状态。

实体状态如何影响数据库

状态不仅仅表示上下文中的实体状态,还表示数据如何持久化到数据库。每一个状态,都有一个对应的SQL命令。

Added状态的实体使用INSERT命令在映射的表中创建一个新行进行持久化。Modified实体在表中已经有了对应的行,所以使用UPDATE命令持久化。Deleted状态的实体在表中有对应的行,但是它触发DELETE而不是UPDATE

DetachedUnchanged状态对数据库没有影响:detached实体不能被上下文追踪,所以不用持久化,而unchanged实体没有修改的东西需要持久化。

在其生存期期间,实体可以改变其状态。

实体生命周期的状态改变

实体的状态可以由上下文(context)自动设置也可以由开发人员手动设置。尽管从一个状态到另一个状态的所有转换组合都是可能的,但是有一些是没有意义的。例如,将一个实体从Added状态转换到Deleted状态是没有意义的,反之亦然。下图展示了所有的状态以及一个实体如何从一个状态传递到另一个状态。

这里写图片描述

图中描述的非常清晰了,唯一需要说明的就是图中所示的所有方法都属于ObjectContext或者ObjectSet<T>类。下面我们详细的看一下各种状态。

DETACHED状态

当一个实体处于Detached状态,它没有绑定到上下文(context),所以它的状态是不能被追踪的。它可以释放,修改以及和其他类组合或者其他任何你可能需要的方式使用。因为它没有上下文追踪它,它对EF没有意义。

由于上下文不能追踪你代码中任何对象的创建,因此,Detached是新创建实体的默认状态。即使你在上下文的using块中实例化实体也是如此。当追踪被禁用时,Detached还是从数据库中检索的实体的状态。

UNCHANGED状态

当实体是Unchanged状态,它被绑定到上下文,但是它还没有被修改。默认情况下,从数据库中检索的实体是这种状态。
当实体附加到(使用Attach方法)上下文时,它同样是Unchanged状态。上下文不能追踪它不引用的对象的变化,所以当它们附加到上下文,它们是Unchanged状态。

ADDED状态

当实体处于Added状态,你有很少的选择。实际上,你只能使用Detach方法将它从上下文中分离。

当然,即使你修改了一些属性,状态依然保持为Added,因为转换到ModifiedUnchanged或者Deleted没有意义——它是一个新实体并且在数据库中没有对应的行。这是处在这些状态的前提条件。

MODIFIED状态

当实体是Modified时,这意味着它处在Unchanged状态,然后改变了一些属性。

一个实体进入Modified状态后,它可以转换到Detached或者Deleted状态,但是即使手动重置初始值也不能使它回滚到Unchanged状态(除非从上下文分离再附加到上下文)。它也不能变成Added状态(除非从上下文分离再添加一个实体到上下文,因为在数据库中已经存在这个ID的行,当持久化它时,就会得到一个运行时异常)。

DELETED状态

实体进入Deleted状态因为它处于Unchanged或者Modified状态,然后使用了DeleteObject。这是最严格的状态,因为除了转换成Detached状态,从这种状态转换成任何其他的状态都是没有意义的。

下一篇文章学习如何管理实体的状态。


(二)

管理实体状态

上下文仅仅自动处理Unchanged状态到Modified状态的转变。其他的状态转变必须使用适当的方法显示处理:

AddObject——在Added状态时给上下文添加一个实体。
Attach——在Unchanged状态时附加一个实体到上下文。
ApplyCurrentValuesApplyOriginalValues——改变状态为Modified,将追踪的实体与另一个比较。
DeleteObject——标记一个实体为Deleted
AcceptAllChanges——标记所有的实体为Unchanged
ChangeStateChangeObjectState——改变一个实体从一个状态到另一个状态没有任何限制(Detached除外)
Detach——从上下文移除一个实体。

这些方法在ObjectContextObjectSet<T>类中公开,AttachToChangeState除外。ObjectSet<T>方法在内部调用上下文的方法,所以两者没有什么区别。

下面我们详细看一下每一个方法。

AddObject方法

AddObject允许在Added状态时给上下文添加一个实体。当实体被添加到上下文,为了修改它会被添加到上下文追踪。当持久化过程被触发,上下文使用INSERT命令保存对象为表中的一个新行。在OrderIT例子中,持久化一个order会引起对Order表的INSERT,然而持久化一个shirt会引起对ProductShirt表的INSERT

AddObject上下文方法接受两个参数,实体集的名称和实体。

public void AddObject(string entitySetName, object entity)

在这段代码中至少有两个缺点。首先,实体集的名字是以字符串传递的。如果输入错误,会在运行时得到异常。第二,Object类型的实体参数,意味着你传递任意CLR类型,如果这个对象不正确只有在运行时才会得到异常。

在强类型时代,这样一个API是难以忍受的。为了克服这个糟糕的设计,EF团队在实体集接口中引进了一个等价的API。它只接受需要添加的对象:

public void AddObject(TEntity entity)

TEntity是由实体集维持的实体类型(记住一个实体集是实现IObjectSet<T>接口类型的实例)。因为强类型,你无法传递一个不正确的对象给方法。此外,实体集知道它的名字,没有必要指定它。

Attach方法

Unchanged状态时,Attach方法将对象附加到上下文。当实体被附加到上下文,它就由上下文追踪对标量属性的修改。

现实世界中的应用程序,一个实体需要被附加有很多情形。Web应用程序和Web服务就是典型的例子。回到前面的例子,在检索客户的方法中,创建一个上下文,执行查询,返回对象给客户端,然后释放上下文。在更新的方法中,创建一个新的上下文,然后将customer附加给它。最后,持久化customer

回到Attach方法,Attach上下文方法接收两个参数:实体集的名称和实体。这个方法遭受AddObject同样的限制,所以已经过时了。取而代之,可以使用Attach实体集方法,它只需附加对象,看下面的清单:

var c = new Customer { CompanyId = 1 };
...
ctx.Companies.Attach(c);

你附加一个对象给上下文,因为你想它的数据在数据库中被更新。当然,对象必须在数据库中有对应的行,这个对应由主键属性和列标识的。

当附加一个对象给上下文时,主键列必须设置,否则会在运行时得到一个InvalidOperationException。而且,在持久性阶段,如果UPDATE命令不能作用于任何行,上下文会抛出异常。

因为附加的实体要离开Unchanged状态,你必须找到一种方式标记它为Modified在数据库里持久化它。下一个要讨论的方法完成这个工作。

ApplyCurrentValuesApplyOriginalValues方法

在我们的客户web服务例子中,当已经附加了对象,就需要持久化它。问题是,它被附加后还是Unchanged状态,所以需要找到一种方式改变它为Modified状态。最简单的方式是在数据库中查询最新的数据并将它与输入的实体比较。

你知道对象上下文保持有一个对每个实体的引用,这个实体或者是从数据中检索出的或者是通过AddObject或者Attach方法附加的。我们没有提到的是,当实体被绑定到上下文时,标量属性的原值(original values)和当前值(current values)保存在内存中。

ApplyOriginalValues方法将实体作为输入(来自数据库)。然后这个方法从上下文的内存中检索一个相同类型以及具有相同键的对象作为输入实体。最后,该方法复制输入实体的标量属性的值给上下文实体的标量属性的原值。目前,保存在上下文中标量属性的原值包含来自数据库的数据,然而保存在上下文中的标量属性的当前值包含来自web服务的实体的值。如果原值不同于当前值,实体就被设置为Modified状态;否则它仍然是Unchanged

也可以按照相反的路径。查询数据库并且从web服务的实体应用修改代替附加实体并且查询数据库。这是ApplyCurrentValues方法所做的事情。它将一个实体作为输入(来自web服务)。然后该方法在上下文内存中检索一个相同类型以及具有相同键的对象作为输入实体。最后,该方法复制输入实体的标量属性值到上下文实体的标量属性的当前值。目前,保存在上下文的当前值包含来自web服务实体的数据,并且原值是来自数据库的值。如果它们不同,实体就被设置为Modified状态,否则,它仍然是Unchanged

当持久化被触发,如果实体是Modified状态,它就用UPDATE命令持久化。

如我们前边讨论的方法,ApplyOriginalValuesApplyCurrentValues方法属于ObjectContextObjectSet<T>类,我们建议使用后者公开的方法,如下:

var entityFromDb = GetEntityFromDb(entityFromService.CompanyId);
ctx.Companies.Attach(entityFromService);
ctx.Companies.ApplyOriginalValues(entityFromDb);
ctx.Companies.First(c => c.CompanyId == entityFromService.CompanyId);
ctx.Companies.ApplyCurrentValues(entityFromService);

这里你必须意识到有一点点的陷阱。两个方法仅仅关心输入实体的标量和复杂属性。如果一个关联实体的标量属性改变,或者在关联集合中一个新行被添加,移除或者修改,它不会被检测到。
DeleteObject方法

DeleteObject方法标记一个实体为Deleted。唯一需要注意的是你必须牢记传递到该方法的实体必须附加到上下文。该对象必须来自查询或者已经使用Attach方法附加到了上下文。如果在上下文中没有找到该对象,就会抛出一个InvalidOperationException异常,附带一条信息:The object cannot be deleted because it was not found in the ObjectStateManager。

下面的清单显示了使用由ObjectSet<T>类公开的DeleteObject方法。

var c = ctx.Companies.OfType<Customer>().Where(w => w.CompanyId == 1); 
ctx.Companies.DeleteObject(c);

var c = new Customer { ... }; 
ctx.Companies.Attach(c); 
ctx.Companies.DeleteObject(c);

DeleteObject被调用,实体没有从上下文删除;它被标记为deleted。当持久化被触发,实体从上下文移除并且执行DELETE命令从数据库删除它。

AcceptAllChanges方法

AcceptAllChanges方法接受所有AddedModified状态的实体并标记它们为Unchanged。然后分离所有Deleted状态的实体,最后更新ObjectStateManager条目。

AcceptAllChangesObjectContext公开,在ObjectSet<T>类中没有对用的方法。这就是为什么需要使用下面的代码:

ctx.AcceptAllChanges();

ChangeStateChangeObjectState方法

ChangeStateChangeObjectState方法是灵活的方法。它们允许改变一个实体的状态到任何其他可能的状态(Detached除外)。当使用一个实体时,这些方法非常有用。不过当处理复杂的对象图时,它们的重要性也增加,这在本章后面讨论。

ChangeStateObjectStateEntry类公开,而ChangeObjectStateObjectStateManager类公开。ChangeState只需要新的状态,因为ObjectStateEntry的实例已经指的是一个实体。ChangeObjectState接受实体和新的状态作为参数。两个方法如下面的清单所示:

var osm = ctx.ObjectStateManager;
osm.ChangeObjectState(entity, EntityState.Unchanged);
osm.GetObjectStateEntry(entity).ChangeState(EntityState.Unchanged);

这些方法并不总是物理的更改实体状态;有时使用先前的方法。例如,改变一个实体的状态为Unchanged意味着调用ObjectStateEntry类的AcceptChanges方法。相反,改变一个实体的状态从UnchangedAdded意味着改变状态。
有时并不需要实体被持久化或者由上下文追踪修改。如果那样,可以将实体从上下文移除。

Detach方法

Detach方法从上下文追踪的实体的列表移除实体。不管实体处于什么状态,它都会变成Detached,但是由分离的(detached)实体引用的实体不能分离(detached)。

调用该方法非常简单,如下面的清单,因为它只接受必要分离的实体。

ctx.Companies.Detach(c);

成功分离的前提条件是实体已经被附加到了上下文。如果没有,就会得到一个InvalidOperationException异常,附带一条信息The object cannot be detached because it is not attached to the Object-StateManager。


(三)

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,OriginalValuesCurrentValues。注意OriginalValueshCurrentValuesDbDataRecord类型的。

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

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

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

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,ModifiedDeleted状态中检索实体并且在日志中写入entry。这个解决方法如下面的清单所示:

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

通常,只需要检索单个entryGetObjectStateEntries在这种情况下不可用。你需要另一个方法,允许传递一个实体,得到相对应的状态管理器(state-manager)的entrystate 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状态时这个方法也由DeleteObjectChangeState调用。
SetModified 标记实体以及它的所有属性为Modified。当移动到Modified时这个方法在内部由ChangeState调用。
SetModifiedProperty 标记一个属性为Modified,因此也标记实体。
AcceptChanges 改变实体的状态为Unchanged并使用当前值重写entry的原值。
ChangeState 改变实体的状态到输入值。

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

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

理解对象跟踪

从技术上来说,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-managerentry,并且比较每一个的原值和存储在实体中的值。当它发现属性被修改——在本例中,是customername——它标记属性为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属性发生了改变。下图包含了一个代理内部简单的代码版本。

这里写图片描述

这个功能很棒,不用费劲就实现了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

如果order使用查询加载,会有一点点不同。state managerordercustomer和它们的关联各创建一个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)属性中如何更改关系

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

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

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

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

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

集合属性调用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值,一个新的关系必须定义,外键属性必须分配到另一个不为空的值,或者不关联的对象必须删除。

虽然这看起来像加密了一样,其实很清晰。detailOrderId属性不能为null,因为你不能有独立的order detaildetail必须分配到一个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为什么不自动删除实体而是简单的移除引用产生疑问。答案是在其他情况下这是不正确的行为。想想supplierproduct的多对多关系。如果是那样,如果移除由supplier卖的product,你不必删除它。你只需删除Product-Supplier表的引用。由于这些不同的行为,EF团队谨慎地决定让你显示选择怎么做。

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

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

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

更改跟踪和MergeOption

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

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

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

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

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

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

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

ctx.ContextOptions.UseLegacyPreserveChangesBehavior = true;

现在已经了解了MergeOption行为。任何应用程序中,它都是重要的一方面,它也经常会被滥用或者被轻视。

阅读更多
个人分类: ADO.NET Entity Framework
上一篇EntityFramework 学习【Entity Lifecycle 实体生命周期】
下一篇EntityFramework Core不得不注意的性能优化意外收获,你会用错?
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭