在 ORM 文献中,有三种方式将对象的继承关系映射到表中。
- 每个类型一张表 TPT: 在继承层次中的每个类都分别映射到数据库中的一张表,彼此之间通过外键关联。
- 继承层次中所有的类型一张表 TPH:对于继承层次中的所有类型都映射到一张表中,所有的数据都在这张表中。
- 每种实现类型一张表 TPC: 有点像其他两个的混合,对于每种实现类型映射到一张表,抽象类型像 TPH 一样展开到表中。
这里我将讨论 TPT 和 TPH,EF 的好处是可以混合使用这些方式。
TPT 方式
让我们从每种类型一张表开始,我定义了一个简单的继承层次,一个抽象基类和两个派生类。
public
abstract
class
PersonBase
{
public
int
PersonID {
get
;
set
; }
[Required]
public
string
FirstName {
get
;
set
; }
[Required]
public
string
LastName {
get
;
set
; }
public
int
Age {
get
;
set
; }
}
public
class
Worker : PersonBase
{
public
decimal
AnnualSalary {
get
;
set
; }
}
public
class
Retired : PersonBase
{
public
decimal
MonthlyPension {
get
;
set
; }
}
你需要告诉模型构建器如何映射到表中。
protected
override
void
OnModelCreating(DbModelBuilder modelBuilder)
{
base
.OnModelCreating(modelBuilder);
modelBuilder.Entity
<
PersonBase
>
().HasKey(x
=>
x.PersonID);
modelBuilder.Entity
<
PersonBase
>
().Property(x
=>
x.PersonID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
//
TPT mapping
modelBuilder.Entity
<
PersonBase
>
().ToTable(
"
tpt.Person
"
);
modelBuilder.Entity
<
Worker
>
().ToTable(
"
tpt.Worker
"
);
modelBuilder.Entity
<
Retired
>
().ToTable(
"
tpt.Retired
"
);
}
我们使用默认的命名映射约定,模型构建器使用这些信息用 TPT 来创建数据库。
我们使用模型来跑一些代码,让我们理解如何使用上面的映射,基本上,我们仅仅使用一个 DbSet,一个 PersonBase 的集合,EF 会管理每一个成员的实际类型。
public
static
void
ManageTPT()
{
using
(var context1
=
new
TptContext())
{
var worker
=
new
Worker
{
AnnualSalary
=
20000
,
Age
=
25
,
FirstName
=
"
Joe
"
,
LastName
=
"
Plumber
"
};
var retired
=
new
Retired
{
MonthlyPension
=
1500
,
Age
=
22
,
FirstName
=
"
Mike
"
,
LastName
=
"
Smith
"
};
//
Make sure the tables are empty…
foreach
(var entity
in
context1.Persons)
{
context1.Persons.Remove(entity);
}
context1.Persons.Add(worker);
context1.Persons.Add(retired);
context1.SaveChanges();
}
using
(var context2
=
new
TptContext())
{
Console.WriteLine(
"
Persons count:
"
+
context2.Persons.OfType
<
PersonBase
>
().Count());
Console.WriteLine(
"
Worker:
"
+
context2.Persons.OfType
<
Worker
>
().Count());
Console.WriteLine(
"
Retired:
"
+
context2.Persons.OfType
<
Retired
>
().Count());
}
}
这真的很强大,我们可以通过访问 Workers 来仅仅访问 Workers 表。
TPH 方式
TPH 是 EF 实际上默认支持的。我们可以简单地注释到前面例子中的对表的映射来使用默认的机制。
protected
override
void
OnModelCreating(DbModelBuilder modelBuilder)
{
base
.OnModelCreating(modelBuilder);
modelBuilder.Entity
<
PersonBase
>
().HasKey(x
=>
x.PersonID);
modelBuilder.Entity
<
PersonBase
>
().Property(x
=>
x.PersonID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
//
TPT mapping
//
modelBuilder.Entity<PersonBase>().ToTable("tpt.Person");
//
modelBuilder.Entity<Worker>().ToTable("tpt.Worker");
//
modelBuilder.Entity<Retired>().ToTable("tpt.Retired");
}
结果是现在使用一张表来影射整个的继承层次。
注意到整个的层次被展开到一张表中。基类中没有的属性被自动标记为可空。还有一个额外的区分列,如果运行前面的例子,我们将会看到这个区分列的内容。
当 EF 读取一行的时候,区分列被 EF 用来知道应该创建实例的类型,因为现在所有的类都被映射到了一张表中。
也可以覆盖这一点,下面我们看一下同时混合使用 TPH 和 TPT。我定义了 Worker 的两个子类,我希望将这两个类和 Worker 基类映射到一张表。
public
class
Manager : Worker
{
public
int
?
ManagedEmployeesCount {
get
;
set
; }
}
public
class
FreeLancer : Worker
{
[Required]
public
string
IncCompanyName {
get
;
set
; }
}
注意到每一个属性都必须是可空的。这在 TPH 中非常不方便:每一个属性都必须是可空的。现在我们使用模型构建器来完成。
protected
override
void
OnModelCreating(DbModelBuilder modelBuilder)
{
base
.OnModelCreating(modelBuilder);
modelBuilder.Entity
<
PersonBase
>
().HasKey(x
=>
x.PersonID);
modelBuilder.Entity
<
PersonBase
>
().Property(x
=>
x.PersonID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
//
TPT mapping
modelBuilder.Entity
<
PersonBase
>
().ToTable(
"
tpt.Person
"
);
modelBuilder.Entity
<
Retired
>
().ToTable(
"
tpt.Retired
"
);
//
TPH mapping
modelBuilder.Entity
<
Worker
>
()
.Map
<
FreeLancer
>
(m
=>
m.Requires(f
=>
f.IncCompanyName).HasValue())
.Map
<
Manager
>
(m
=>
m.Requires(ma
=>
ma.ManagedEmployeesCount).HasValue())
.ToTable(
"
tph.Worker
"
);
}
这里我使用了一种区分的方法:与默认不同,我要求属于类的列是非空的列。
使用者不需要与 TPT 区分,甚至在修改了映射之后不会影响使用的代码。
public
static
void
ManageTPH()
{
using
(var context1
=
new
HierarchyContext())
{
var worker
=
new
Worker
{
AnnualSalary
=
20000
,
Age
=
25
,
FirstName
=
"
Joe
"
,
LastName
=
"
Plumber
"
};
var freeLancer
=
new
FreeLancer
{
Age
=
22
,
FirstName
=
"
Mike
"
,
LastName
=
"
Smith
"
,
IncCompanyName
=
"
Mike & Mike Inc
"
};
var manager
=
new
Manager
{
Age
=
43
,
FirstName
=
"
George
"
,
LastName
=
"
Costanza
"
,
ManagedEmployeesCount
=
12
};
//
Make sure the tables are empty…
foreach
(var entity
in
context1.Persons)
{
context1.Persons.Remove(entity);
}
context1.Persons.Add(worker);
context1.Persons.Add(freeLancer);
context1.Persons.Add(manager);
context1.SaveChanges();
}
using
(var context2
=
new
HierarchyContext())
{
Console.WriteLine(
"
Persons count:
"
+
context2.Persons.OfType
<
PersonBase
>
().Count());
Console.WriteLine(
"
Worker:
"
+
context2.Persons.OfType
<
Worker
>
().Count());
Console.WriteLine(
"
Retired:
"
+
context2.Persons.OfType
<
Retired
>
().Count());
Console.WriteLine(
"
FreeLancer:
"
+
context2.Persons.OfType
<
FreeLancer
>
().Count());
Console.WriteLine(
"
Manager:
"
+
context2.Persons.OfType
<
Manager
>
().Count());
}
}
SQL 中的架构如下,这里混合使用了 TPT 和 TPH 。