5

我在项目中使用实体框架 4.3.1,首先使用代码和 DbContext API。我的应用程序是一个 n 层应用程序,其中断开连接的对象可能来自客户端。我正在使用 SQL Server 2008 R2,但很快将迁移到 SQL Azure。我遇到了一个我似乎无法解决的问题。

想象一下我有几节课:

class A {
    // Random stuff here
}
class B {
    // Random stuff here
    public A MyA { get; set; }
}
class C {
    // Random stuff here
    public A MyA { get; set; }
}

默认情况下,EF 对对象图进行操作。例如,如果我有一个 B 的实例,它封装了 A 的一个实例并且我调用myDbSet.Add(myB);,它也会将 A 的实例标记为正在添加(假设它尚未被跟踪)。

我的应用程序中有一个场景,我需要明确哪些对象被持久化到数据库中,而不是让它跟踪整个对象图。操作顺序如下:

A myA = new A(); // Represents something already in DB that doesn't need to be udpated.
C myC = new C() { // Represents something already in DB that DOES need to be updated.
    A = myA;
}
B myB0 = new B() { // Not yet in DB.
    A = myA;
}
B myB1 = new B() { // Not yet in DB.
    A = myA;
}

myDbSetC.Attach(myC);
context.Entry(myC).State = Modified;

myDbSetB.Add(myB0); // Tries to track myA with a state of Added
myDbSetB.Add(myB1);

context.SaveChanges();

此时我收到一条错误消息,说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. 我相信会发生这种情况,因为在 myB0 上调用 add 会将 A 的实例标记为已添加,这与已跟踪的 A 实例冲突。

理想情况下,我可以做类似 call的事情myDbSet.AddOnly(myB),但显然我们没有那个选项。

我尝试了几种解决方法:

尝试#1: 首先,我尝试创建一个辅助方法来防止 myA 被第二次添加。

private void MarkGraphAsUnchanged<TEntity>(TEntity entity) where TEntity : class {
        DbEntityEntry entryForThis = this.context.Entry<TEntity>(entity);
        IEnumerable<DbEntityEntry> entriesItWantsToChange = this.context.ChangeTracker.Entries().Distinct();

        foreach (DbEntityEntry entry in entriesItWantsToChange) {
            if (!entryForThis.Equals(entry)) {
                entry.State = System.Data.EntityState.Unchanged;
            }
        }
    }

...

myDbSetB.Add(myB0);
MarkGraphAsUnchanged(myB0);

虽然这解决了它尝试添加 myA 的问题,但它仍然会导致 ObjectStateManager 中的键违规。

尝试 #2: 我尝试执行与上述相同的操作,但将状态设置为 Detached 而不是 Unchanged。这适用于保存,但它坚持设置myB0.A = null,这对我的代码有其他不利影响。

尝试 #3: 我在整个 DbContext 周围使用了 TransactionScope。但是,即使SaveChanges()在每个Attach()and之间调用Add(),更改跟踪器也不会刷新其跟踪的条目,所以我遇到了与尝试 #1 相同的问题。

尝试#4: 我继续使用 TransactionScope,除了我使用存储库/DAO 模式并在内部创建一个新的 DbContext 并调用SaveChanges()我所做的每个不同的操作。在这种情况下,我收到错误消息“存储更新、插入或删除语句影响了意外的行数。” 使用 SQL Profiler 时,我发现在调用我SaveChanges()执行的第二个操作(第一个Add())时,它实际上第二次将UPDATESQL 从第一个操作发送到数据库——但不会更改任何行。对我来说,这感觉就像实体框架中的一个错误。

尝试 #5: 我决定只使用 DbTransaction,而不是使用 TransactionScope。我仍然创建多个上下文,但是在创建每个新上下文时将一个预构建的 EntityConnection 传递给它(通过缓存和手动打开由第一个上下文构建的 EntityConnection)。但是,当我这样做时,第二个上下文会运行我定义的初始化程序,即使它在应用程序首次启动时已经运行。在开发环境中,我有一些测试数据的种子,它实际上超时等待我第一次Attach()修改的表上的数据库锁定(但由于事务仍然打开而仍然被锁定)。

帮助!!我已经尝试了所有我能想到的东西,但没有完全重构我的应用程序以不使用导航属性或使用手动构建的 DAO 来执行 INSERT、UPDATE 和 DELETE 语句,我不知所措。似乎必须有一种方法可以让实体框架为 O/R 映射带来好处,但仍然需要手动控制事务中的操作!

4

2 回答 2

2

一定有其他你没有显示的东西,因为你附加和添加实体的方式没有问题。以下代码将附加myA、、myC和作为未更改的上下文并将状态设置myB0为已修改。myB1myC

myDbSetC.Attach(myC);
context.Entry(myC).State = Modified;

以下代码将正确检测到所有实体都已附加,而不是抛出异常(如在 ObjectContext API 中所做的那样)或再次插入所有实体(如您所料),它只会更改myB0myB1添加状态:

myDbSetB.Add(myB0);
myDbSetB.Add(myB1);

如果您的myAmyC已使用现有实体的键正确初始化,则整个代码将正确执行并保存,除了单个问题:

C myC = new C() { 
    A = myA;
}

这看起来像独立关联和独立关联有自己的状态,但设置其状态的API 在 DbContext API 中不可用。如果这是您要保存的新关系,它将不会被保存,因为它仍被跟踪为未更改。您必须使用外键关联,或者必须将上下文转换为ObjectContext

ObjectContext objectContext = ((IObjectContextAdapter)dbContext).ObjectContext;

并用于ObjectStateManager改变关系的状态

于 2012-03-01T22:01:16.137 回答
0

正如 Ladislav 建议的那样,我得到了一致的对象实例,这解决了它试图添加冗余 As 的问题。

事实证明,B0 和 B1 实际上都封装了其他对象(分别为 D0 和 D1),而这些对象又封装了 A。D0 和 D1 都已经在数据库中,但未被 Entity 跟踪。

添加 B0/B1 导致 D0/D1 也被错误地插入。我最终使用对象上下文 API Ladislav 建议将 D0/D1 的 ObjectStateEntry 标记为 Unchanged,并将 D0/D1 和 A 之间的关系标记为 Unchanged。这似乎可以满足我的需要:仅更新 C 并插入 B0/B1。

下面是我执行此操作的代码,我在 SaveChanges 之前调用它。请注意,我确信仍然有一些边缘情况没有处理,而且这还没有经过全面测试——但它应该可以大致了解需要做什么。

// Entries are put in here when they are explicitly added, modified, or deleted.
private ISet<DbEntityEntry> trackedEntries = new HashSet<DbEntityEntry>();
private void MarkGraphAsUnchanged()
{
    IEnumerable<DbEntityEntry> entriesItWantsToChange = this.context.ChangeTracker.Entries().Distinct();
    foreach (DbEntityEntry entry in entriesItWantsToChange)
    {
        if (!this.trackedEntries.Contains(entry))
        {
            entry.State = System.Data.EntityState.Unchanged;
        }
    }

    IEnumerable<ObjectStateEntry> allEntries =
            this.context.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added)
            .Union(this.context.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted))
            .Union(this.context.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified));

        foreach (ObjectStateEntry entry in allEntries)
        {
            if (entry.IsRelationship)
            {
                /* We can't mark relationships are being unchanged if we are truly adding or deleting the entity.
                 * To determine this, we need to first lookup the entity keys, then state entries themselves.
                 */
                EntityKey key1 = null;
                EntityKey key2 = null;
                if (entry.State == EntityState.Deleted)
                {
                    key1 = (EntityKey)entry.OriginalValues[0];
                    key2 = (EntityKey)entry.OriginalValues[1];
                }
                else if (entry.State == EntityState.Added)
                {
                    key1 = (EntityKey)entry.CurrentValues[0];
                    key2 = (EntityKey)entry.CurrentValues[1];
                }

                ObjectStateEntry entry1 = this.context.ObjectContext.ObjectStateManager.GetObjectStateEntry(key1);
                ObjectStateEntry entry2 = this.context.ObjectContext.ObjectStateManager.GetObjectStateEntry(key2);

                if ((entry1.State != EntityState.Added) && (entry1.State != EntityState.Deleted) && (entry2.State != EntityState.Added) && (entry2.State != EntityState.Deleted))
                {
                    entry.ChangeState(EntityState.Unchanged);
                }
            }
        }
    }

哇!!!基本模式是:

  1. 明确跟踪所做的更改。
  2. 返回并清理 Entity 认为它需要做的所有事情,但实际上并没有。
  3. 实际上将更改保存到数据库中。

这种必须“返回并清理”的方法显然不是最佳选择,但它似乎是目前最好的选择,在我尝试任何保存操作之前无需手动附加外围实体(例如 D0/D1)。将所有这些逻辑放在通用存储库中会有所帮助——逻辑只需要编写一次。我确实希望在未来的版本中,Entity 可以直接添加此功能(并消除关于在堆上具有多个对象实例但具有相同键的限制)。

于 2012-03-02T21:32:01.100 回答