在使用Entity Framework(EF) 开发Web应用程序时,经常需要处理对象图,例如订单(Order)和订单行(OrderLines),以及一些只读的引用数据,如客户(Customer)和产品(Products)。通常情况下,会从数据库查询这些对象图,然后发送给客户端(浏览器)。当客户端将这些对象图返回给服务器时,希望将其持久化到数据库中,这就要求首先将这些对象图附加到DbContext。本文将介绍一种简单有效的方法来实现这一过程,避免重新从数据库加载数据,从而提高性能。
在处理订单和订单行的父-子关系时,通常会使用EF查询数据库。但是,当客户端将这些数据发送回服务器时,希望更新数据库,而不需要重新从数据库加载整个对象图。重新加载数据不仅会影响性能,而且可能会破坏数据的完整性。幸运的是,经过深入研究,找到了一种解决方案。
在EF中,有两种方法可以将分离的对象附加到DbContext:Add()和Attach()。Add()方法会将整个对象图附加到DbContext,并将所有对象标记为新增(Added),而Attach()方法也会将整个对象图附加到DbContext,但会将所有对象标记为未修改(Unchanged)。由于对象组通常包含新的、修改的和未修改的数据,只能选择这两种方法中的一种来附加整个对象图,然后遍历对象图并纠正每个条目的状态。
但是,Attach()方法可能会导致键冲突,因为相同的对象类型可能会有重复的键值。例如,如果有一个订单,它包含两个新的订单行,这些订单行的Id可能都是0。使用Attach()方法附加这个订单会导致失败,因为Attach()会将这两个订单行标记为未修改,而EF要求所有现有实体都应该有唯一的主键。因此,选择使用Add()方法来附加。
问题在于,如何知道对象图中每个对象的状态(新/修改/未修改/已删除)?由于分离的对象不会被跟踪,唯一可靠的方法就是重新从数据库加载对象图,而之前已经明确表示不希望这样做,因为这会影响性能。
可以使用一个简单的约定:如果Id大于0,则对象被视为已修改,如果Id等于0,则对象被视为新的。这是一个相当简单的约定,但也有一些缺点:无法检测到未修改的对象,因此可能会将未修改的数据保存到数据库中。不过,这些对象图通常不会太大,因此这不应该是性能问题。删除对象必须使用自定义逻辑来处理,例如,有一个名为DeletedOrderLines的集合。
每个对象图都可能包含引用数据(只读)。例如,当保存订单时,可能在对象图中有产品和客户对象,但知道不想将它们保存到数据库中。知道只想保存订单和订单行。另一方面,EF并不知道这一点。这就是AttachByIdValue方法接受一个子类型数组的原因,这些子类型应该与根实体一起保存。所有不在根实体也不是子类型的图对象都将附加到上下文中,但将被标记为未修改,因此它们不会被保存到数据库中。
以下是AttachByIdValue方法的实现,该方法使用实体的Id来确定实体是新的还是已修改的。如果Id为零,则实体被视为新的,否则被视为已修改的。如果想要保存的不仅仅是根实体,那么必须提供子类型。如果对象图中的实体不是根实体也不是子类型,它将被附加但不会被保存(它将被视为未修改的)。
public static void AttachByIdValue(thisDbContextcontext, TEntity rootEntity, HashSet childTypes) where TEntity : class, IEntity {
// mark root entity as added
this.action adds whole graph and marks each entity in it as added
context.Set().Add(rootEntity);
// in case root entity has id value mark it as modified (otherwise it stays added)
if (rootEntity.Id != 0) {
context.Entry(rootEntity).State = EntityState.Modified;
}
// traverse all entities in context (hopefully they are all part of graph we just attached)
foreach (var entry in context.ChangeTracker.Entries()) {
// we are only interested in graph we have just attached
// and we know they are all marked as Added
// and we will ignore root entity because it is already resolved correctly
if (entry.State == EntityState.Added && entry.Entity != rootEntity) {
// if no child types are defined for saving then just mark all entities as unchanged
if (childTypes == null || childTypes.Count == 0) {
entry.State = EntityState.Unchanged;
} else {
// request object type from context
// because we might have got reference to dynamic proxy
// and we wouldn't want to handle Type of dynamic proxy
Type entityType = ObjectContext.GetObjectType(entry.Entity.GetType());
// if type is not child type than it should not be saved so mark it as unchanged
if (!childTypes.Contains(entityType)) {
entry.State = EntityState.Unchanged;
} else if (entry.Entity.Id != 0) {
// if entity should be saved with root entity
// then if it has id mark it as modified
// else leave it marked as added
entry.State = EntityState.Modified;
}
}
}
}
}