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

时间:2013-08-07 10:21:38

标签: 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; }
}

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

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

如何使用EntityFramework 5?

感谢您的帮助!

1 个答案:

答案 0 :(得分:6)

我没有一个好的答案,但我能想到四个解决方案"这确实是解决方法:

  1. 不要为主键使用DBMS计算值(如果您已经使用了自然键,那很好)。
  2. 使用DBMS计算的代理键。
  3. 遵循state pattern
  4. 之类的内容
  5. object state manager做一些邪恶的伏都教。
  6. 更新:似乎有一种普遍的共识,即尝试甚至不值得;因此,大多数人只是使用存储过程来解决问题。

    使用自然键

    首先,请记住,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();
                }
            }
        }
    }
    

    输出:

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

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

    注意:我使用Table注释子类,因为您提到了TPT设置,但问题实际上与TPT无关。

    使用代理键

    如果您认为代理密钥是真正的内部,那么只要您仍然可以以相同的方式访问您的数据,它就不会发生变化(使用例如二级索引。

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

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

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

    输出:

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

    使用状态模式(或类似)

    这个解决方案可能是大多数人会考虑的正确解决方案&#34;,因为你无法在大多数面向对象的语言中改变对象的内在类型。这是CTS - 兼容语言的情况,包括C#。

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

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

    使用内部EF伏都教

    我很快查看了DbContextObjectContextObjectStateManager的参考文档,我无法立即找到任何方法来更改一个实体。如果您的运气比我好,您可以使用DTO和DbPropertyValues进行转换。

    重要提示

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

    结论

    当你做任何不平凡的事情时,EF并不灵活,但它会不断改进。希望这个答案在未来不会有用。我也可能不知道现有的杀手功能会使你想要的东西成为可能,所以不要根据这个答案做出任何决定。