我没有一个好的答案,但我可以想到四个真正的解决方法:
- 不要对主键使用 DBMS 计算的值(如果您已经使用自然键,那很好)。
- 使用 DBMS 计算的代理键。
- 遵循类似状态模式的东西。
- 用对象状态管理器做一些邪恶的巫术。
更新:似乎有一个普遍的共识,即尝试甚至不值得;因此,大多数人只是简单地使用存储过程来解决这个问题。
使用自然键
首先,请记住 EF 跟踪的对象是您的 DAL 的一部分,而不是您的域模型(无论您是否使用 POCO)。有些人不需要域模型,但请记住这一点,因为我们现在可以将这些对象视为表记录的表示,我们以我们不会使用域对象的方式进行操作。
在这里,我们使用IDbSet.Remove
删除实体的记录,然后使用相同的主键添加新的记录IDbSet.Add
,所有这些都在一个事务中。请参阅ChangeType
下面示例代码中的方法。
从理论上讲,完整性是可以的,从理论上讲,EF 可以检测您正在尝试做的事情并优化事情。实际上,它目前没有(我分析了 SQL 接口来验证这一点)。结果是它看起来很丑(DELETE
+INSERT
而不是UPDATE
),所以如果系统美观和性能是问题,它可能是不行的。如果你能接受它,它是相对简单的。
这是我用来测试的一些示例代码(如果您想试验,只需创建一个新的控制台应用程序,添加对EntityFramework
程序集的引用,然后粘贴代码)。
A
是基类,X
是Y
子类。我们认为Id
是一个自然键,所以我们可以在子类的复制构造函数中复制它(这里只实现了 for 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 无关。
使用代理键
如果您认为代理键是真正的 internal,那么只要您仍然可以以相同的方式访问您的数据(例如使用二级索引),它是否在您的眼皮底下发生变化并不重要。
注意:在实践中,很多人会到处泄露代理键(域模型、服务接口……)。不要这样做。
如果您采用前面的示例,只需删除子类型的复制构造函数中的DatabaseGenerated
属性和赋值即可。Id
注意:由于其值由 DBMS 生成,EF 完全忽略了该属性,并且除了由模型构建器分析以在 SQL 模式中Id
生成列之外,它没有任何实际用途。Id
那并被糟糕的程序员泄露。
输出:
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 巫术
我很快浏览了DbContext
,ObjectContext
和的参考文档ObjectStateManager
,但我无法立即找到任何方法来更改实体的类型。如果你的运气比我好,你也许可以使用 DTO 并DbPropertyValues
进行转换。
重要的提示
使用前两种解决方法,您可能会遇到一系列导航属性和外键问题(因为DELETE
+INSERT
操作)。这将是一个单独的问题。
结论
当你做任何不平凡的事情时,EF 并不是那么灵活,但它会不断改进。希望这个答案在将来不会相关。我也可能不知道现有的杀手级功能可以使您想要的成为可能,因此请不要根据此答案做出任何决定。