实体框架5每个表类型更新,更改子类型但保持相同的基本类型

Fab*_*Fab 8 entity-framework table-per-type

我有一个简单的层次结构

public abstract class CommunicationSupport
{
    public SupportTypeEnum Type { get; set; }
    public Country Origin { get; set; } // National or Foreign support
}

public class TelecomSupport : CommunicationSupport
{
    public string Number { get; set; }
}

public class PostalSupport : CommunicationSupport
{
    public Address Address { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

我计划为我的数据库使用Table-per-type层次结构.因此将创建3个表,一个基数和两个子项使用与基础相同的PK.

我的问题是我希望能够通过更改它的类型来更新CommunicationSupport.假设我创建了一个TelecomSupport,将其保存,然后将其类型更改为PostalSupport并再次保存(更新).我期望的结果是EF保持相同的基本记录(CommunicationSupport Id),但删除TelecomSupport表中的记录并在PostalSupport中创建一个新记录.因此,TelecomSupport和PostalSupport是独家的,不能共享相同的基础CommunicationSupport.

我如何使用EntityFramework 5来做到这一点?

谢谢你的帮助!

tne*_*tne 6

我没有一个好的答案,但我可以想到四个真正解决方法的"解决方案":

  1. 不要对主键使用DBMS计算值(如果您已使用自然键,则没关系).
  2. 使用DBMS计算的代理键.
  3. 遵循状态模式之类的东西.
  4. 对象状态管理器做一些邪恶的伏都教.

更新:似乎有一种普遍的共识,即尝试甚至不值得; 因此,大多数人只是使用存储过程来解决问题.

使用自然键

首先,请记住,EF跟踪的对象是DAL的一部分,而不是您的域模型(无论您是否使用POCO).有些人不需要域模型,但要记住它,因为我们现在可以将这些对象视为表记录的表示,我们以不使用域对象的方式操作.

在这里,我们使用IDbSet.Remove删除实体的记录,然后IDbSet.Add在单个事务中使用相同的主键添加新的记录.请参阅ChangeType下面的示例代码中的方法.

从理论上讲,完整性是可以的,理论上,EF可以检测出您正在尝试做的事情并优化事物.在实践中,它目前没有(我分析了SQL接口来验证这一点).结果是它看起来很丑(DELETE+ INSERT而不是UPDATE),所以如果系统美和性能都是问题,那么它可能是不行的.如果你能接受它,那就相对简单了.

下面是我用来测试它的一些示例代码(如果您想进行实验,只需创建一个新的控制台应用程序,添加对EntityFramework程序集的引用,然后粘贴代码).

A是基类,XY是子类.我们认为Id它是一个自然键,因此我们可以将它复制到子类复制构造函数中(这里仅实现Y).代码创建一个数据库,并使用类型记录对其进行种子处理X.然后,它运行并将其类型更改为Y,显然X在此过程中丢失了特定数据.复制构造函数是转换数据的位置,如果数据丢失不是业务流程的一部分,则将其存档.唯一的"有趣"代码是ChangeType方法,其余的是样板.

using System;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Linq;

namespace EntitySubTypeChange {
    abstract class A {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int Id { get; set; }
        public string Foo { get; set; }
        public override string ToString() {
            return string.Format("Type:\t{0}{3}Id:\t{1}{3}Foo:\t{2}{3}",
                this.GetType(), Id, Foo, Environment.NewLine);
        }
    }

    [Table("X")]
    class X : A {
        public string Bar { get; set; }
        public override string ToString() {
            return string.Format("{0}Bar:\t{1}{2}", base.ToString(), Bar, Environment.NewLine);
        }
    }

    [Table("Y")]
    class Y : A {
        public Y() {}
        public Y(A a) {
            this.Id = a.Id;
            this.Foo = a.Foo;
        }

        public string Baz { get; set; }
        public override string ToString() {
            return string.Format("{0}Baz:\t{1}{2}", base.ToString(), Baz, Environment.NewLine);
        }
    }

    class Program {
        static void Main(string[] args) {
            Display();
            ChangeType();
            Display();
        }

        static void Display() {
            using (var context = new Container())
                Console.WriteLine(context.A.First());
            Console.ReadKey();
        }

        static void ChangeType()
        {
            using (var context = new Container()) {
                context.A.Add(new Y(context.A.Remove(context.X.First())));
                context.SaveChanges();
            }
        }

        class Container : DbContext {
            public IDbSet<A> A { get; set; }
            public IDbSet<X> X { get; set; }
            public IDbSet<Y> Y { get; set; }
        }

        static Program() {
            Database.SetInitializer<Container>(new ContainerInitializer());
        }

        class ContainerInitializer : DropCreateDatabaseAlways<Container> {
            protected override void Seed(Container context) {
                context.A.Add(new X { Foo = "Base Value", Bar = "SubType X Value" });
                context.SaveChanges();
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

Type:   EntitySubTypeChange.X
Id:     0
Foo:    Base Value
Bar:    SubType X Value

Type:   EntitySubTypeChange.Y
Id:     0
Foo:    Base Value
Baz:
Run Code Online (Sandbox Code Playgroud)

注意: 如果你想要一个自动生成的自然键,你不能让EF要求DBMS计算它,否则EF会阻止你按照你想要的方式操作它(见下文).实际上,EF会将具有计算值的所有键视为代理键,即使它仍然很乐意泄露它们(两个世界的坏处).

注意: 我注释子类是Table因为你提到了TPT设置,但问题实际上与TPT无关.

使用代理键

如果您认为代理密钥是真正的内部密钥,那么只要您仍然可以以相同的方式访问数据(例如使用辅助索引),它是否会在您的鼻子下发生变化并不重要.

注意:在实践中,许多人泄露代理键(域模型,服务接口,......).不要这样做.

如果您采用上一个示例,只需删除子类型的复制构造函数中的DatabaseGenerated属性和赋值Id.

注意:使用DBMS生成的值时Id,EF将完全忽略该属性,除了由模型构建器分析以Id在SQL模式中生成列之外,它不会用于任何实际目的.这和坏程序员泄露.

输出:

Type:   EntitySubTypeChange.X
Id:     1
Foo:    Base Value
Bar:    SubType X Value

Type:   EntitySubTypeChange.Y
Id:     2
Foo:    Base Value
Baz:
Run Code Online (Sandbox Code Playgroud)

使用状态模式(或类似)

这个解决方案可能是大多数人认为的"正确解决方案",因为在大多数面向对象的语言中你无法改变对象的内在类型.这是符合CTS标准的语言,包括C#.

问题是这种模式在域模型中正确使用,而不是像在EF中实现的那样在DAL中使用.我不是说这是不可能的,你可以用复杂的类型或TPH结构来破解,以避免创建一个中间表,但很可能你会在河上游泳直到你放弃.希望有人可以证明我错了.

注意:您可以决定希望关系模型看起来不同,在这种情况下,您可以完全绕过此问题.但这不是你问题的答案.

使用内部EF伏都教

我很快就浏览了参考文档DbContext,ObjectContext并且ObjectStateManager,我无法立即找到改变实体类型的方法.如果你有比我更好的运气,你可以使用DTO并DbPropertyValues进行转换.

重要的提示

使用前两个解决方法,您可能会遇到一些导航属性和外键问题(因为DELETE+ INSERT操作).这将是一个单独的问题.

结论

当你做任何不平凡的事情时,EF并不灵活,但它会不断改进.希望这个答案将来不会有用.我也可能没有意识到现有的杀手功能可以实现你想要的功能,所以不要根据这个答案做出任何决定.