实体框架 - 避免“查找”实体在最初加载到另一个上下文时获得添加状态

时间:2013-02-01 14:21:27

标签: c# entity-framework

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

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充当查找表,其中记录链接到其他记录,但记录永远不会通过应用程序创建。

我遇到的问题是在不同的上下文中加载查找实体时,例如当它被缓存时。因此,保存Record的上下文不跟踪该实体,并且在调用GraphRoot DbSet时调用Add时,查找最终的EntityState为Added,但实际上它应该是Unchanged。

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

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

2 个答案:

答案 0 :(得分: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.
}

<强> EDIT2:

以下代码显示了预加载参考数据如何解决您的问题。

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);

答案 1 :(得分:1)

是Lookup定义为外键吗? 这个代码是第一个吗? 如果是这样,请尝试更改子项以使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);

OR

如果您想获得当前的工作方法,您可以尝试   将Lookup项附加到Context FIRST。   确保它没有标记,然后添加图形;)