实体框架 - 避免“查找”实体在另一个上下文中加载时被添加状态

3
这是一些演示我问题的测试代码:

using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using NUnit.Framework;

namespace EFGraphInsertLookup
{
    public class GraphLookup
    {
        public int ID { get; set; }
        public string Code { get; set; }
    }

    public class GraphChild
    {
        public int ID { get; set; }
        public virtual GraphRoot Root { get; set; }
        public virtual GraphLookup Lookup { get; set; }
    }

    public class GraphRoot
    {
        public int ID { get; set; }
        public virtual ICollection<GraphChild> Children { get; set; }
    }

    public class TestDbContext : DbContext
    {
        public DbSet<GraphRoot>   GraphRoots    { get; set; }
        public DbSet<GraphChild>  GraphChildren { get; set; }
        public DbSet<GraphLookup> GraphLookups  { get; set; }

        public TestDbContext()
        {
            GraphLookups.ToList();
        }
    }

    public class TestDbInit : DropCreateDatabaseAlways<TestDbContext>
    {
        protected override void Seed(TestDbContext context)
        {
            base.Seed(context);
            context.GraphLookups.Add(new GraphLookup { Code = "Lookup" });
            context.SaveChanges();
        }
    }

    [TestFixture]
    public class Tests
    {
        [Test]
        public void MainTest()
        {
            Database.SetInitializer<TestDbContext>(new TestDbInit());

            var lookupCtx = new TestDbContext();
            var firstLookup = lookupCtx.GraphLookups.Where(l => l.Code == "Lookup").Single();

            var graph = new GraphRoot
            {
                Children = new List<GraphChild> { new GraphChild { Lookup = firstLookup } }
            };
            var ctx = new TestDbContext();
            ctx.GraphRoots.Add(graph); // Creates a new lookup record, which is not desired
            //ctx.GraphRoots.Attach(graph); // Crashes due to dupe lookup IDs
            ctx.SaveChanges();

            ctx = new TestDbContext();
            graph = ctx.GraphRoots.Single();
            Assert.AreEqual(1, graph.Children.First().Lookup.ID, "New lookup ID was created...");
        }
    }
}

我的愿望是让GraphLookup作为一个查找表,其中记录与其他记录链接,但从应用程序中永远不会创建记录。

我遇到的问题是,在不同的上下文中加载查找实体时(例如在缓存中),当执行记录的保存操作时,上下文无法跟踪该实体,并且当调用GraphRoot DbSet的Add方法时,查找结果会被添加到EntityState中,但实际上它应该是未更改的。

如果我尝试使用attach,由于两个查找实体最终进入上下文,因此会导致重复键而崩溃。

有什么解决方法吗?请注意,我已经大大简化了实际的问题。在我的实际应用程序中,这发生在EF DBContext的多个不同层的存储库、工作单元和业务服务类之间。因此,最好能够应用于DBContext中的通用解决方案。

2个回答

3
如果您要将现有实体(例如来自缓存)带入另一个 DbContext,则必须显式管理实体状态。这导致两个简单的结论:除非确实需要,否则不要混合来自多个上下文的实体,当您这样做时,请显式设置附加的每个实体状态
您可以尝试使用以下一种缓存方法。创建一个简单的缓存管理器类,可能是静态的。对于您想要缓存的每个实体类型,都有一个类似于此的 GetMyEntity(int myEntityId, DbContext context) 方法:
public MyEntity GetMyEntity(int entityId, MyContext context)
{
    MyEntity entity;

    // Get entity from context if it's already loaded.
    entity = context.Set<MyEntity>().Loaded.SingleOrDefault(q => q.EntityId == entityId);

    if (entity != null)
    {
        return entity;
    }
    else if (this.cache.TryGetValue("MYENTITY#" + entityId.ToString(), out entity)
    {
        // Get entity from cache if it's present.  Adapt this to whatever cache API you're using.
        context.Entry(entity).EntityState = EntityState.Unchanged;
        return entity;
    }
    else
    {
        // Load entity if it's not in the context already or in the cache.
        entity = context.Set<MyEntity>().Find(entityId);

        // Add loaded entity to the cache.  Adapt this to specify suitable rules for cache item expiry if appropriate.
        this.cache["MYENTITY#" + entityId.ToString()] = entity;
        return entity;
    }
}

请原谅任何打字错误,但是希望你能够理解。你可能会发现这可以泛化,这样你就不必为每个实体类型拥有一个方法。编辑:下面的代码可能有用,它演示了如何分离除了你想要添加的实体之外的所有东西。
// Add a single entity.
context.E1s.Add(new1);

var dontAddMeNow = (from e in context.ChangeTracker.Entries()
                    where !object.ReferenceEquals(e.Entity, new1)
                    select e).ToList();

foreach (var e in dontAddMeNow)
{
    e.State = System.Data.EntityState.Unchanged;  // Or Detached.
}

编辑2:

这里是代码,展示如何预加载参考数据来解决你的问题。

E2 child = new E2 { Id = 1 };

context.Entry(child).State = System.Data.EntityState.Unchanged;

E1 new1 = new E1
{
    Child = child
};

// Add a single entity.
context.E1s.Add(new1);

Debug.Assert(context.Entry(new1.Child).State == System.Data.EntityState.Unchanged);
Debug.Assert(context.Entry(new1).State == System.Data.EntityState.Added);

这有道理,Olly。我希望找到一些东西,在添加发生后但在 SaveChanges 之前可以修复状态。这样,用户代码就不必更改。此外,缓存并不是唯一出现此问题的时候。当 UI 加载查找项(例如下拉列表)然后将这些查找项推送到添加中时,也可能会出现此问题。 - RationalGeek
@RationalGeek - 是的。很可能没有一个“万应钥匙”来解决这个问题。这是一种情况,EF无法确定您要做什么;它只能尽最大努力猜测。 - Olly
我在概念上同意这个想法。但是我很难确切地弄清楚如何实现它。但是,我同意采取这种方法。 - RationalGeek
关于提前加载参考数据,我似乎无法使其正常工作。即使我提前加载它,最终仍然会得到状态为“已添加”的参考实体。 - RationalGeek
关于你的第一个编辑,我尝试了非常相似的东西,但是由于这个原因它没有起作用。根据我的示例中的实体,我有一个单一的Root,有许多Children,并且Children指向不同的Lookups。现有的Children已经有了附加到上下文的Lookups。当我尝试添加新的Children并将这些Children的Lookups设置为未更改时,我会收到重复键异常,因为有两个附加到具有相同ID的上下文的未更改Lookups,它不知道该怎么办。 - RationalGeek
显示剩余4条评论

1

查找是否定义为外键? 这是Code First吗? 如果是这样,请尝试更改子项以具有LookupID而不仅仅是导航属性。
然后只提供GraphLookiD。(性能更好,因为不需要先加载查找实体。)

public class GraphChild
{
    public int ID { get; set; }
    public int GraphLookupId  { get; set; } //<<<<< add this an SET ONLY this
    public virtual GraphRoot Root { get; set; }
    public virtual GraphLookup Lookup { get; set; }
}

实体GraphCHILD的流畅API片段

  .HasRequired(x => x.Lookup).WithMany().HasForeignKey(x => x.graphlookupID);

如果您想让当前方法起作用,可以尝试先将查找项附加到上下文中。确保它没有被标记,然后再添加图表 ;)


这是一个好主意。它很可能会奏效。不幸的是,我无法保证我的业务服务的消费者只设置ID。但是,当特定情况被识别出来时,我至少可以使用它来解决问题。 - RationalGeek

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