【问题标题】:Entity Framework - Avoid "lookup" entities getting Added state when originally loaded in another context实体框架 - 避免“查找”实体在最初加载到另一个上下文时获得添加状态
【发布时间】:2013-02-01 14:21:27
【问题描述】:

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

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 结束,但实际上它应该是 Unchanged。

如果我改为尝试使用附加,则会由于重复键而导致崩溃,因为两个查找实体最终在上下文中。

解决此问题的最佳方法是什么?请注意,我已经相当简化了实际问题。在我的实际应用程序中,这是通过位于 EF DBContext 之上的几个不同的存储库层、工作单元和业务服务类来实现的。因此,我可以在 DBContext 中以某种方式应用的通用解决方案将是更可取的。

【问题讨论】:

    标签: c# entity-framework


    【解决方案1】:

    如果您将现有实体(例如,从缓存中)带入另一个 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);
    

    【讨论】:

    • 这很有道理,奥利。我希望在添加发生后但在 SaveChanges 之前找到可以修复状态的东西。这样消费者代码就不必改变了。此外,缓存不是我遇到此问题的唯一时间。当 UI 加载查找时也会发生这种情况,比如进入下拉列表,然后将这些查找推送到添加中。
    • @RationalGeek - 对。可能没有解决这个问题的“灵丹妙药”。在这种情况下,EF 无法确定您要做什么;这是一个最好的猜测。
    • @RationalGeek - 从概念上讲,如果您有一个内部使用 EF 上下文的存储库之类的东西,那么您的调用者不必担心遵守某些规则以避免 EF 错误。因此,使用 EF 的代码必须负责适当地设置实体状态。只有它能够知道什么是“参考”数据和什么是“交易”数据。前者需要将其状态设置为未更改(或完全分离)。我喜欢 soadyp 的想法,即预先将“参考”数据加载到上下文中,因此它永远不会被标记为添加。
    • 我在概念上同意这一点。我很难弄清楚如何到达那里。但是是的,我同意这种方法。
    • 关于提前加载参考数据,我似乎无法做到这一点。即使我提前加载它,我仍然会得到状态为 added 的引用实体。
    【解决方案2】:

    查找是否定义为外键? 这段代码是第一个吗? 如果是这样,请尝试将子项更改为具有 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 sn-p

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

    如果您想采用当前的工作方式,您可以尝试 首先将查找项附加到上下文。 确保它没有被标记为然后添加图表;)

    【讨论】:

    • 这是个好主意。它可能会起作用。不幸的是,我不能保证我的业务服务的消费者只会设置 ID。但我至少可以在识别出特定情况时使用它来解决它们。
    猜你喜欢
    • 1970-01-01
    • 2015-04-26
    • 2010-10-16
    • 1970-01-01
    • 1970-01-01
    • 2023-04-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多