将导航属性设置为null后未能保存至数据库。

4
我很新于 EF,遇到了一个实体并未如我所料的情况,希望能帮我理解这是 EF 中的 bug 还是我的“误解”。本文结尾附上所有代码,可在安装 Entity 6 的测试项目中进行编译和运行。
以下是具体情况:
我正在使用 Code-First。
我有两个实体处于多对一的关系。它们如下:
public class OneSideEntity
    {
        public int OneSideEntityId { get; set; }
        public string Name { get; set; }
        public virtual List<ManySideEntity> MyManySideEntities { get; set; }
    }

    public class ManySideEntity
    {
        public int ManySideEntityId { get; set; }
        public string Name { get; set; }
        public virtual OneSideEntity MyOneSideEntity { get; set; }
    }

假设我创建并保存了一个ManySideEntity到变量many中,我在执行context.SaveChanges()之后就处理了上下文。然后我通过对manyMyOneSideEntity属性执行“set”来创建并保存一个OneSideEntity,在之后处理上下文时进行处置。这很好;新的OneSideEntity实例被添加到数据库中,并且many的外键正确更新为OneSideEntity实例的主键值。
这里开始变得有趣了。如果我现在尝试使用新的上下文设置many.MyOneSideEntity = null;并进行保存,更改不会推送到数据库中,单元测试中的asserts RelationshipRemovalFails将失败。
然而,如果我通过使用关系的另一端进行移除,它就可以工作。也就是说,如果我获取OneSideEntity实例,称之为one,并从其单侧导航属性中删除它,如one.MyManySideEntities.Remove(many);并保存,它确实会被推送到数据库中。因此,与此场景相对应的单元测试中的asserts(在下面的代码中称为RelationshipRemovalSucceeds1)通过了,因为更改已经保存。实体甚至根据我的理解正确地更新了many.MyOneSideEntity为null作为保存的副作用。
最后,如果同一个失败的单元测试稍微更改一下,使用相同的DbContext添加one并通过many.MyOneSideEntity = null;进行删除,它也会成功。这个单元测试在下面的代码中被称为RelationshipRemovalSucceeds2
我认为应该能够使失败的案例工作,并且应该更新单侧实体的导航属性。有没有一种方法可以将导航属性设置为null,并让Entity在不必保持相同的DbContext的情况下推出更改?
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.ModelConfiguration;
using System.Linq;
using Epsilon.Toolbox.LINQ;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Tests
{
    ////////////////////////////////////////////////
    // Two simple entity classes
    ////////////////////////////////////////////////

    public class OneSideEntity
    {
        public int OneSideEntityId { get; set; }
        public string Name { get; set; }
        public virtual List<ManySideEntity> MyManySideEntities { get; set; }
    }

    public class ManySideEntity
    {
        public int ManySideEntityId { get; set; }
        public string Name { get; set; }
        public virtual OneSideEntity MyOneSideEntity { get; set; }
    }

    ////////////////////////////////////////////////
    // Fluent configuration classes for the entities
    ////////////////////////////////////////////////

    public class ManySideEntityConfiguration : EntityTypeConfiguration<ManySideEntity>
    {
        public ManySideEntityConfiguration()
        {
            this.HasKey( x => x.ManySideEntityId );
        }
    }

    public class OneSideEntityConfiguration : EntityTypeConfiguration<OneSideEntity>
    {
        public OneSideEntityConfiguration()
        {
            this.HasKey( x => x.OneSideEntityId );
        }
    }

    ////////////////////////////////////////////////
    // DbContext 
    ////////////////////////////////////////////////

    public class RelationshipDeleteTestContext : DbContext
    {
        public DbSet<OneSideEntity> OneSideEntities { get; set; }
        public DbSet<ManySideEntity> ManySideEntities { get; set; }

        protected override void OnModelCreating( DbModelBuilder modelBuilder )
        {
            // Entities
            modelBuilder.Configurations.Add( new OneSideEntityConfiguration() );
            modelBuilder.Configurations.Add( new ManySideEntityConfiguration() );
        }
    }

    ////////////////////////////////////////////////
    // Fails to properly save the result of "manySideEntityX.MyOneSideEntity = null;"
    ////////////////////////////////////////////////

    [TestClass]
    public class EntityTest
    {
        [TestMethod]
        [TestCategory( "EntityTests" )]
        public void RelationshipRemovalFails()
        {
            Database.SetInitializer( new DropCreateDatabaseAlways<RelationshipDeleteTestContext>() );

            // Add a ManySideEntity.
            int manySideEntityXId;
            using ( var context = new RelationshipDeleteTestContext() )
            {
                var manySideEntityX1 = new ManySideEntity() { Name = @"X" };
                context.ManySideEntities.Add( manySideEntityX1 );
                context.SaveChanges();
                manySideEntityXId = manySideEntityX1.ManySideEntityId;
            }

            // Add a OnSideEntity by setting to the ManySide entity's 
            // navigation property to the newly created OneSideEntity.
            int oneSideEntityIdA;
            using ( var context = new RelationshipDeleteTestContext() )
            {
                var oneSideEntityA = new OneSideEntity()
                {
                    Name = "A",
                };
                var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
                manySideEntityX.MyOneSideEntity = oneSideEntityA;
                context.SaveChanges();
                oneSideEntityIdA = oneSideEntityA.OneSideEntityId;
            }

            int oneSideEntityIdB;
            using ( var context = new RelationshipDeleteTestContext() )
            {
                var oneSideEntityB = new OneSideEntity()
                {
                    Name = "B",
                };
                var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
                manySideEntityX.MyOneSideEntity = oneSideEntityB;
                context.SaveChanges();
                oneSideEntityIdB = oneSideEntityB.OneSideEntityId;
            }

            using ( var context = new RelationshipDeleteTestContext() )
            {
                var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );

                // Here is the statement that doesn't work; the database is not updated to null out the foreign key after SaveChanges.
                manySideEntityX.MyOneSideEntity = null;
                context.SaveChanges();
            }

            using ( var context = new RelationshipDeleteTestContext() )
            {
                var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
                var oneSideEntityB = context.OneSideEntities.Single( x => x.OneSideEntityId == oneSideEntityIdB );
                var manySideCount = oneSideEntityB.MyManySideEntities.Count();

                // Both Asserts fail since the foreign key in the ManySideEntities table has not been nulled out.
                Assert.IsNull( manySideEntityX.MyOneSideEntity );
                Assert.AreEqual( 0, manySideCount );
            }
        }

        [TestMethod]
        [TestCategory( "EntityTests" )]
        public void RelationshipRemovalSucceeds1()
        {
            Database.SetInitializer( new DropCreateDatabaseAlways<RelationshipDeleteTestContext>() );

            // Add a ManySideEntity.
            int manySideEntityXId;
            using ( var context = new RelationshipDeleteTestContext() )
            {
                var manySideEntityX = new ManySideEntity() { Name = @"X" };
                context.ManySideEntities.Add( manySideEntityX );
                context.SaveChanges();
                manySideEntityXId = manySideEntityX.ManySideEntityId;
            }

            // Add a OnSideEntity by setting to the ManySide entity's 
            // navigation property to the newly created OneSideEntity.
            int oneSideEntityIdA;
            using ( var context = new RelationshipDeleteTestContext() )
            {
                var oneSideEntityA = new OneSideEntity()
                {
                    Name = "A",
                };
                var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
                manySideEntityX.MyOneSideEntity = oneSideEntityA;
                context.SaveChanges();
                oneSideEntityIdA = oneSideEntityA.OneSideEntityId;
            }

            int oneSideEntityIdB;
            using ( var context = new RelationshipDeleteTestContext() )
            {
                var oneSideEntityB = new OneSideEntity()
                {
                    Name = "B",
                };
                var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
                manySideEntityX.MyOneSideEntity = oneSideEntityB;
                context.SaveChanges();
                oneSideEntityIdB = oneSideEntityB.OneSideEntityId;
            }

            using ( var context = new RelationshipDeleteTestContext() )
            {
                var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );

                // Using the other side of the relationship DOES work!
                var oneSideEntityB = manySideEntityX.MyOneSideEntity;
                oneSideEntityB.MyManySideEntities.Remove( manySideEntityX );
                context.SaveChanges();
            }

            using ( var context = new RelationshipDeleteTestContext() )
            {
                var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
                var oneSideEntityB = context.OneSideEntities.Single( x => x.OneSideEntityId == oneSideEntityIdB );
                var manySideCount = oneSideEntityB.MyManySideEntities.Count();
                // Asserts now succeed!
                Assert.IsNull( manySideEntityX.MyOneSideEntity );
                Assert.AreEqual( 0, manySideCount );
            }
        }

        [TestMethod]
        [TestCategory( "EntityTests" )]
        public void RelationshipRemovalSucceeds2()
        {
            Database.SetInitializer( new DropCreateDatabaseAlways<RelationshipDeleteTestContext>() );

            // Add a ManySideEntity.
            int manySideEntityXId;
            using ( var context = new RelationshipDeleteTestContext() )
            {
                var manySideEntityX = new ManySideEntity() { Name = @"X" };
                context.ManySideEntities.Add( manySideEntityX );
                context.SaveChanges();
                manySideEntityXId = manySideEntityX.ManySideEntityId;
            }

            // Add a OnSideEntity by setting to the ManySide entity's 
            // navigation property to the newly created OneSideEntity.
            int oneSideEntityIdA;
            using ( var context = new RelationshipDeleteTestContext() )
            {
                var oneSideEntityA = new OneSideEntity()
                {
                    Name = "A",
                };
                var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
                manySideEntityX.MyOneSideEntity = oneSideEntityA;
                context.SaveChanges();
                oneSideEntityIdA = oneSideEntityA.OneSideEntityId;
            }

            int oneSideEntityIdB;
            using ( var context = new RelationshipDeleteTestContext() )
            {
               {
                    var oneSideEntityB = new OneSideEntity()
                    {
                        Name = "B",
                    };
                    var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
                    manySideEntityX.MyOneSideEntity = oneSideEntityB;
                    context.SaveChanges();
                    oneSideEntityIdB = oneSideEntityB.OneSideEntityId;
                }

                {
                    var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );

                    // This now works when using the same context for an add the remove.
                    manySideEntityX.MyOneSideEntity = null;
                    context.SaveChanges();
                }
            }

            using ( var context = new RelationshipDeleteTestContext() )
            {
                var manySideEntityX = context.ManySideEntities.Single( x => x.ManySideEntityId == manySideEntityXId );
                var oneSideEntityB = context.OneSideEntities.Single( x => x.OneSideEntityId == oneSideEntityIdB );
                var manySideCount = oneSideEntityB.MyManySideEntities.Count();

                // Both Asserts now Succeed/
                Assert.IsNull( manySideEntityX.MyOneSideEntity );
                Assert.AreEqual( 0, manySideCount );
            }

        }
    }
}
2个回答

6
我已经弄清楚了发生了什么。 当我设置many.MyOneSideEntity = null;时,实体的更改跟踪器捕获了“null”的初始值。 因此,从实体的角度来看,属性值没有变化,因此更改跟踪器认为不需要更新属性的值,即使属性的值与数据库中的值不同。

要解决这个问题,只需通过getter访问属性。 这会导致实体的状态加载到内存中,并将更改跟踪器与这些值同步。 然后,更改跟踪器注意到通过将属性设置为null所产生的更改,然后在调用SaveChanges时将更改推送到数据库。

稍微有点奇怪,但它符合实体更改跟踪器的工作方式。

希望这能帮助其他人!


2
如果您有一个FK ID字段(MyOneSideEntityId),您只需将其设置为null,SaveChanges应该会删除引用。如果您更新ID字段而不是实体(就像您所说的那样,如果您通过实体进行操作,则必须首先从数据库中加载它),则更改跟踪器的工作效果更好。 - Flater
这只是垃圾,它使得从ObjectContext迁移大型代码库到dbContext变得不可能,因为以前可以通过设置null来解决...有任何替代方案吗? - Michiel Cornille

0

你似乎没有在这两个类之间定义关系。你缺少类似以下的定义:

.HasRequired(...).WithMany(...)

更多信息请参见此链接


1
据我所知,Entity使用约定(在这种情况下,存在导航属性)来推断关系。如果您查看创建的数据库,您将看到类之间正确的外键关系。添加此内容不会改变行为。感谢您抽出时间尝试回答! - Paul

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