(一)
我们先从分析实体的生命周期和它的状态开始。
实体生命周期
在其生存期期间,一个实体只有一个状态。在了解如何检索状态之前,先看看什么是实体状态。实体状态就是声明为以下值的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命令保存那些Added,Modified或者Deleted状态。
实体状态如何影响数据库
状态不仅仅表示上下文中的实体状态,还表示数据如何持久化到数据库。每一个状态,都有一个对应的SQL命令。
Added状态的实体使用INSERT命令在映射的表中创建一个新行进行持久化。Modified实体在表中已经有了对应的行,所以使用UPDATE命令持久化。Deleted状态的实体在表中有对应的行,但是它触发DELETE而不是UPDATE。
Detached和Unchanged状态对数据库没有影响:detached实体不能被上下文追踪,所以不用持久化,而unchanged实体没有修改的东西需要持久化。
在其生存期期间,实体可以改变其状态。
实体生命周期的状态改变
实体的状态可以由上下文(context)自动设置也可以由开发人员手动设置。尽管从一个状态到另一个状态的所有转换组合都是可能的,但是有一些是没有意义的。例如,将一个实体从Added
状态转换到Deleted
状态是没有意义的,反之亦然。下图展示了所有的状态以及一个实体如何从一个状态传递到另一个状态。
图中描述的非常清晰了,唯一需要说明的就是图中所示的所有方法都属于ObjectContext
或者ObjectSet<T>
类。下面我们详细的看一下各种状态。
DETACHED状态
当一个实体处于Detached
状态,它没有绑定到上下文(context),所以它的状态是不能被追踪的。它可以释放,修改以及和其他类组合或者其他任何你可能需要的方式使用。因为它没有上下文追踪它,它对EF没有意义。
由于上下文不能追踪你代码中任何对象的创建,因此,Detached
是新创建实体的默认状态。即使你在上下文的using
块中实例化实体也是如此。当追踪被禁用时,Detached
还是从数据库中检索的实体的状态。
UNCHANGED状态
当实体是Unchanged
状态,它被绑定到上下文,但是它还没有被修改。默认情况下,从数据库中检索的实体是这种状态。
当实体附加到(使用Attach
方法)上下文时,它同样是Unchanged
状态。上下文不能追踪它不引用的对象的变化,所以当它们附加到上下文,它们是Unchanged
状态。
ADDED状态
当实体处于Added
状态,你有很少的选择。实际上,你只能使用Detach
方法将它从上下文中分离。
当然,即使你修改了一些属性,状态依然保持为Added
,因为转换到Modified
,Unchanged
或者Deleted
没有意义——它是一个新实体并且在数据库中没有对应的行。这是处在这些状态的前提条件。
MODIFIED状态
当实体是Modified
时,这意味着它处在Unchanged
状态,然后改变了一些属性。
一个实体进入Modified
状态后,它可以转换到Detached
或者Deleted
状态,但是即使手动重置初始值也不能使它回滚到Unchanged
状态(除非从上下文分离再附加到上下文)。它也不能变成Added
状态(除非从上下文分离再添加一个实体到上下文,因为在数据库中已经存在这个ID的行,当持久化它时,就会得到一个运行时异常)。
DELETED状态
实体进入Deleted
状态因为它处于Unchanged
或者Modified
状态,然后使用了DeleteObject
。这是最严格的状态,因为除了转换成Detached
状态,从这种状态转换成任何其他的状态都是没有意义的。
下一篇文章学习如何管理实体的状态。
(二)
管理实体状态
上下文仅仅自动处理Unchanged
状态到Modified
状态的转变。其他的状态转变必须使用适当的方法显示处理:
AddObject
——在Added
状态时给上下文添加一个实体。
Attach
——在Unchanged
状态时附加一个实体到上下文。
ApplyCurrentValues
和ApplyOriginalValues
——改变状态为Modified
,将追踪的实体与另一个比较。
DeleteObject
——标记一个实体为Deleted
。
AcceptAllChanges
——标记所有的实体为Unchanged
。
ChangeState
和ChangeObjectState
——改变一个实体从一个状态到另一个状态没有任何限制(Detached
除外)
Detach
——从上下文移除一个实体。
这些方法在ObjectContext
和ObjectSet<T>
类中公开,AttachTo
和ChangeState
除外。ObjectSet<T>
方法在内部调用上下文的方法,所以两者没有什么区别。
下面我们详细看一下每一个方法。
AddObject
方法
AddObject
允许在Added
状态时给上下文添加一个实体。当实体被添加到上下文,为了修改它会被添加到上下文追踪。当持久化过程被触发,上下文使用INSERT
命令保存对象为表中的一个新行。在OrderIT
例子中,持久化一个order
会引起对Order
表的INSERT
,然而持久化一个shirt
会引起对Product
和Shirt
表的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
在数据库里持久化它。下一个要讨论的方法完成这个工作。
ApplyCurrentValues
和ApplyOriginalValues
方法
在我们的客户web服务例子中,当已经附加了对象,就需要持久化它。问题是,它被附加后还是Unchanged
状态,所以需要找到一种方式改变它为Modified
状态。最简单的方式是在数据库中查询最新的数据并将它与输入的实体比较。
你知道对象上下文保持有一个对每个实体的引用,这个实体或者是从数据中检索出的或者是通过AddObject
或者Attach
方法附加的。我们没有提到的是,当实体被绑定到上下文时,标量属性的原值(original values)和当前值(current values)保存在内存中。
ApplyOriginalValues
方法将实体作为输入(来自数据库)。然后这个方法从上下文的内存中检索一个相同类型以及具有相同键的对象作为输入实体。最后,该方法复制输入实体的标量属性的值给上下文实体的标量属性的原值。目前,保存在上下文中标量属性的原值包含来自数据库的数据,然而保存在上下文中的标量属性的当前值包含来自web服务的实体的值。如果原值不同于当前值,实体就被设置为Modified
状态;否则它仍然是Unchanged
。
也可以按照相反的路径。查询数据库并且从web服务的实体应用修改代替附加实体并且查询数据库。这是ApplyCurrentValues
方法所做的事情。它将一个实体作为输入(来自web服务)。然后该方法在上下文内存中检索一个相同类型以及具有相同键的对象作为输入实体。最后,该方法复制输入实体的标量属性值到上下文实体的标量属性的当前值。目前,保存在上下文的当前值包含来自web服务实体的数据,并且原值是来自数据库的值。如果它们不同,实体就被设置为Modified
状态,否则,它仍然是Unchanged
。
当持久化被触发,如果实体是Modified
状态,它就用UPDATE
命令持久化。
如我们前边讨论的方法,ApplyOriginalValues
和ApplyCurrentValues
方法属于ObjectContext
和ObjectSet<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
方法接受所有Added
和Modified
状态的实体并标记它们为Unchanged
。然后分离所有Deleted
状态的实体,最后更新ObjectStateManager
条目。
AcceptAllChanges
由ObjectContext
公开,在ObjectSet<T>
类中没有对用的方法。这就是为什么需要使用下面的代码:
ctx.AcceptAllChanges();
ChangeState
和ChangeObjectState
方法
ChangeState
和ChangeObjectState
方法是灵活的方法。它们允许改变一个实体的状态到任何其他可能的状态(Detached
除外)。当使用一个实体时,这些方法非常有用。不过当处理复杂的对象图时,它们的重要性也增加,这在本章后面讨论。
ChangeState
由ObjectStateEntry
类公开,而ChangeObjectState
由ObjectStateManager
类公开。ChangeState
只需要新的状态,因为ObjectStateEntry
的实例已经指的是一个实体。ChangeObjectState
接受实体和新的状态作为参数。两个方法如下面的清单所示:
var osm = ctx.ObjectStateManager;
osm.ChangeObjectState(entity, EntityState.Unchanged);
osm.GetObjectStateEntry(entity).ChangeState(EntityState.Unchanged);
这些方法并不总是物理的更改实体状态;有时使用先前的方法。例如,改变一个实体的状态为Unchanged
意味着调用ObjectStateEntry
类的AcceptChanges
方法。相反,改变一个实体的状态从Unchanged
到Added
意味着改变状态。
有时并不需要实体被持久化或者由上下文追踪修改。如果那样,可以将实体从上下文移除。
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
)负责与上下中对象追踪有关的一切:
- 当添加,附加到上下文或者从上下文中删除一个实体,实际上是对
state manager
做的这些。 - 当我们说上下文保留从数据库中读取的所有实体集合在内存中时,其实是
state manager
保存这些数据。 - 当上下文执行一个身份地图(
identity-map
)检查,其实是state manager
执行的检查。 - 当我们说上下文跟踪实体间关系式,其实是
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
。这个解决方法如下面的清单所示:
第一步是挂钩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
。这种通知机制并不总是起作用——它依赖于你如何初始化实体。可以创建下面类型的实体:
- 没有被代理包装POCO实体(普通实体)
- 由代理包装的实体(代理实体)
包装的实体是一个类,它通过代理启用扩展性。当类的继承不是封闭的和它的属性是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
属性发生了改变。下图包含了一个代理内部简单的代码版本。
这个功能很棒,不用费劲就实现了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 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
看起来如下图:
当实体是从数据库中检索的,在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
。在两种情况下可以找到自己:
Customer
已经附加到上下文。如果外键关联起作用,属性是同步的,Order
对象变成Modified
。如果使用独立关联,实体间只有RelationshipEntry
被创建。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团队谨慎地决定让你显示选择怎么做。
当添加一个实体到集合属性,你可以在两种不同的情况下找到自己,依赖于实体是否附加到上下文:
detail
被附加到上下文。如果外键关联起作用,属性必须与order
的ID同步。如果使用独立关联,只需在实体间创建RelationishipEntry
。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
行为。任何应用程序中,它都是重要的一方面,它也经常会被滥用或者被轻视。