【问题标题】:Entity Framework 5 table-per-type update, change sub type but keep same base typeEntity Framework 5 table-per-type 更新,更改子类型但保持相同的基本类型
【发布时间】:2013-08-08 16:18:58
【问题描述】:

我有一个简单的层次结构

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 做到这一点?

感谢您的帮助!

【问题讨论】:

  • 不知道为什么投反对票,投反对票,冒着看起来幼稚的风险;我认为这是一个很好的问题。

标签: entity-framework table-per-type


【解决方案1】:

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

  1. 不要对主键使用 DBMS 计算的值(如果您已经使用自然键,也可以)。
  2. 使用 DBMS 计算的代理键。
  3. 关注state pattern 之类的内容。
  4. object state manager 做一些邪恶的巫术。

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

使用自然键

首先,请记住 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 的赋值即可。

注意:Id 属性是由 DBMS 生成的,它完全被 EF 忽略,除了由模型构建器分析以生成 @ 之外,没有任何实际用途SQL 模式中的 987654357@ 列。并且被糟糕的程序员泄露。

输出:

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

Type:   EntitySubTypeChange.Y
Id:     2
Foo:    Base Value
Baz:

使用状态模式(或类似的)

这种解决方案可能是大多数人认为的“正确解决方案”,因为在大多数面向对象的语言中,您无法更改对象的内在类型。符合 CTS 的语言就是这种情况,其中包括 C#。

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

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

使用内部 EF 巫术

我很快浏览了DbContextObjectContextObjectStateManager 的参考文档,但我无法立即找到更改实体类型的任何方法。如果你运气比我好,你也许可以使用 DTO 和DbPropertyValues 进行转换。

重要提示

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

结论

当你做任何不平凡的事情时,EF 并不是那么灵活,但它会不断改进。希望这个答案在将来不会相关。也有可能我不知道现有的杀手功能可以使您想要的成为可能,因此请不要根据此答案做出任何决定。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-12-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多