使用Entity Framework Code First保存单个对象

5

我在一个项目中使用Entity Framework 4.3.1,使用Code First和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 操作对象图。例如,如果我有一个封装 A 实例的 B 实例,并调用 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无法继续,因为对象的键值与ObjectStateManager中的另一个对象冲突。在调用AcceptChanges之前,请确保键值是唯一的。”我认为这是因为对myB0调用add方法将A的实例标记为已添加,这与已跟踪的A实例冲突。
理想情况下,我可以像调用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);

尝试 #1: 我尝试将myA的状态设置为Unchanged,然后使用Attach(myA),但出现ObjectStateManager中的键冲突。虽然解决了添加myA的问题,但仍然在ObjectStateManager中引起了键冲突。
尝试 #2: 我尝试像上面那样操作,但将状态设置为Detached而不是Unchanged。这适用于保存,但它坚持要将myB0.A = null,这对我的代码有其他不利影响。
尝试 #3: 我在整个DbContext周围使用TransactionScope。但是,即使在每个Attach()和Add()之间调用SaveChanges(),更改跟踪器也不会刷新其已跟踪的条目,因此我与尝试#1中相同的问题。
尝试 #4: 我继续使用TransactionScope,但使用存储库/ DAO模式,并在内部创建一个新的DbContext并为我执行的每个不同操作调用SaveChanges()。在这种情况下,我收到错误“存储更新、插入或删除语句影响了意外数量的行。”当使用SQL Profiler时,我发现在第二个操作(第一个Add())上调用SaveChanges()时,它实际上将UPDATE SQL从第一个操作再次发送到数据库,但不更改任何行。这让我觉得Entity Framework存在缺陷。
尝试 #5: 我决定仅使用DbTransaction而不是TransactionScope。我仍然创建多个上下文,但在每次创建新上下文时将预构建的EntityConnection传递给它(通过缓存并手动打开由第一个上下文构建的EntityConnection)。但是,当我这样做时,第二个上下文会运行我定义的初始化程序,即使它已经在应用程序启动时运行过了。在dev环境中,我有一些测试数据进行种植,它实际上超时等待数据库锁定我第一个Attach()修改的表(但由于事务仍处于打开状态而仍被锁定)。
救命啊!!我尝试了我能想到的所有方法,除了完全重构我的应用程序以不使用导航属性或使用手动构建的DAO执行INSERT、UPDATE和DELETE语句外,我束手无策。似乎必须有一种方法才能获得Entity Framework用于O / R映射的好处,但仍然手动控制事务内部的操作!

你尝试过直接附加myA吗?在附加其他任何内容之前,您需要这样做。 - cadrell0
2个回答

2
必须有其他你没有展示的东西,因为你附加和添加实体的方式没有问题。以下代码将把myAmyCmyB0myB1作为未更改的实体附加到上下文,并将myC的状态设置为已修改。
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改变关系的状态

你是正确的,这些是独立的关联。你说:“如果你的myA和myC用现有实体的键正确初始化”。我的假设是更改跟踪器/状态管理器使用这些键来标识不同的条目。经过进一步分析(我的实际对象图更加复杂),有一种情况是存在两个对象,我们叫它们A。它们具有相同的键和内容,但在内存中是不同的实例。这会导致问题:重复的A与B一起添加,尽管已经有一个A被跟踪并具有相同的键。 - James D. Schwarzmeier
你不能有两个相同实体的实例。这将始终导致问题或异常。 - Ladislav Mrnka
谢谢你的提示!我会检查对象实例是否相同,看看是否还有问题。很遗憾这个限制存在,并且没有办法绕过更改跟踪并手动执行每个操作。我仍然认为在同一事务中存在多个上下文的一些问题...虽然这些问题可能与多个实例的问题不同。无论如何,如果确保实例相同可以解决问题,我会接受答案。再次感谢! - James D. Schwarzmeier

0

正如Ladislav所建议的那样,我让对象实例保持一致,解决了它尝试添加重复As的问题。

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

添加B0/B1导致D0/D1被错误地插入。最终,我使用了Ladislav建议的对象上下文API,将D0/D1的ObjectStateEntry标记为未更改,并将D0/D1与A之间的关系标记为未更改。这似乎可以达到我想要的效果:只更新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可以直接添加此功能(并删除关于在堆上具有相同键的对象的多个实例的限制)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接