当属性类型更改时不更新鉴别器。

3
我有一个实体,其中一个属性是抽象类型。这会创建一个使用表继承的一对一关系。看起来一切都正常。
我可以创建一个项目并将“Base”属性设置为“ConcreteOne”,一切都保存正常。但是,当我尝试更新“Base”为“ConcreteTwo”时,EF会在数据库中更新“Base”记录与新用户值,它不会更新类型的鉴别器。因此,“ConcreteTwo”的额外数据得到了持久化,但鉴别器仍然显示“ConcreteOne”。
以下是一个简单的示例,展示了这个问题。
namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            App_Start.EntityFrameworkProfilerBootstrapper.PreStart();

            Database.SetInitializer(new DropCreateDatabaseAlways<DataContext>());

            // Create our item with ConcreteOne for Base
            using (var context = new DataContext())
            {
                var item = new Item
                    {
                        Base = new ConcreteOne { Name = "Item", Data = 3 }
                    };
                context.Items.Add(item);
                context.SaveChanges();
            }

            // Update Base with a new ConcreteTwo
            using (var context = new DataContext())
            {
                var item = context.Items.FirstOrDefault();

                var newBase = new ConcreteTwo()
                    {
                        Item = item,  
                        Name = "Item 3", 
                        User = new User { Name = "Foo" }
                    };

                // If I don't set this to null, EF tries to create a new record in the DB which causes a PK exception
                item.Base.Item = null;  
                item.Base = newBase;

                // EF doesn't save the discriminator, but DOES save the User reference
                context.SaveChanges();
            }

            // Retrieve the item -- EF thinks Base is still ConcreteOne
            using (var context = new DataContext())
            {
                var item = context.Items.FirstOrDefault();
                Console.WriteLine("{0}: {1}", item.Name, item.Base.Name);
            }

            Console.WriteLine("Done.");
            Console.ReadLine();
        }
    }

    public class DataContext : DbContext
    {
        public DbSet<Item> Items { get; set; }
    }

    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Item
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public virtual Base Base { get; set; }
    }

    public abstract class Base
    {
        public int Id { get; set; }
        public string Name { get; set; }

        [Required]
        public virtual Item Item { get; set; }
    }

    public class ConcreteOne : Base
    {
        public int Data { get; set; }
    }

    public class ConcreteTwo : Base
    {
        public virtual User User { get; set; }
    }
}

当更改被保存时,EF会生成以下SQL:

update [dbo].[Bases]
set    [Name] = 'Item 3' /* @0 */,
       [User_Id] = 1 /* @1 */
where  (([Id] = 1 /* @2 */)
        and [User_Id] is null)

所以它几乎是正确的,但我希望在更新语句中看到[Discriminator] = 'ConcreteTwo'。我的期望是不是没有根据或者我做错了什么?
作为一个测试,我尝试使用表分离策略,然后这个条目从ConcreteOne表中删除并添加到ConcreteTwo表中,正如我所期望的那样。所以它可以工作,但我的真实应用程序至少有七个子类型,并且检索Base属性的SQL语句变得非常恶心。因此,如果可能的话,我肯定想使用TPH来完成这个任务。
更新:我已经验证了这个问题存在于EF5和EF6中。

1
如果 EF 支持从一种类型更新到另一种类型,那就太糟糕了!EF 真是个讨厌的家伙。看起来它的工作方式是不更新它,这是预期的。 - Phill
一辆汽车不能变成一艘船。如果一个人的主要交通工具发生了改变,他并不会神奇地把他的汽车变成一艘船。他需要购买第二种交通工具。 - Phill
1
如果你正在使用 PrimaryTransportation 作为类型,那么这是一个糟糕的设计。 - Phill
我同意一个人的汽车不会变成一艘船,但他们的主要交通工具可能会。我会使用PrimaryTransportation作为属性,它将是Vehicle类型。我不在乎EF是否想删除ConcreteOne记录并创建ConcreteTwo记录;那没关系。我的观点是,它既不使用TPH进行前者,也不使用TPT进行后者。 - Andorbal
@Phill 将这个问题解读为多对一,是我的错误,请忽略这些评论。 - SteveChapman
显示剩余5条评论
3个回答

0
将以下内容添加到您的模型中:

public enum BaseType
{
    ConcreteOne = 1,
    ConcreteTwo = 2
}

public abstract class Base
{
   ...
   public BaseType BaseType { get; set; }
   ...
}

并在 OnModelCreating 方法中:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Base>()
                .ToTable("Base");

    modelBuilder.Entity<ConcreteOne>()
                .Map(t => t.Requires(m => m.BaseType).Equals(BaseType.ConcreteOne))
                .ToTable("ConcreteOne");

    modelBuilder.Entity<ConcreteTwo>()
                .Map(t => t.Requires(m => m.BaseType).Equals(BaseType.ConcreteTwo))
                .ToTable("ConcreteTwo");
}

这将导致EF使用表格继承模式,但我想避免这种情况。我已经验证了TPT按预期工作,但我正在尝试使用TPH来完成此操作。 - Andorbal

0

这个问题基于一个似乎有争议的期望,即更新将会发生。如果TPH层次结构没有按预期工作,并且考虑到EF6目前处于测试版阶段,您最好的选择是在Codeplex论坛上开始讨论。


我会查看论坛,看看是否有其他人遇到了这个问题,但我已经验证了该问题也出现在EF5中。此外,我只是希望我的新对象得到持久化,旧对象被删除。我开始走更新路径,因为EF在保存基表中的更改时生成了一个UPDATE语句。 - Andorbal
我想到了另外一件事。以下代码是否有效?(我现在无法自己运行它) - SteveChapman

-1
我期望这将创建一个具有ConcreteTwo鉴别器的新实例(记录)。
using (var context = new DataContext())
{
    var item = context.Items.FirstOrDefault();
    var newBase = new ConcreteTwo()
    {
        Name = "Item 3", 
        User = new User { Name = "Foo" }
    };

    item.Base = newBase;

    context.SaveChanges();
}

我认为这也可以正常工作,但事实并非如此。该代码将导致PK异常,因为它试图在未删除原始ConcreteOne记录的情况下插入新的ConcreteTwo记录。对于一对一关系,从属记录的PK应等于主记录。如果我在设置item.Base = newBase之前添加item.Base.Item = null行,则EF会将原始标记为已删除,将新标记为已添加。但然后它将其转换为UPDATE语句... - Andorbal
抱歉,我忽略了一对一的情况,尽管已经说明了...我的错;我将Base读作多对一中的父项。那么这种行为是可以预料的。如果您将属性Base更改为Concrete2的新实例,则原始Concrete1实例指向哪个Item实例?我认为这是一个模型问题,而不是EF问题。 - SteveChapman
我希望原始的ConcreteOne实例消失;因此,ConcreteOne的实例将不再引用任何Item。明确一下,我的意图是丢弃ConcreteOne的实例,并完全用ConcreteTwo的实例替换它。 - Andorbal

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