如何有效地为数据库中的继承建模?

在数据库中对继承建模的最佳实践是什么?

什么是权衡(例如可查询性) ?

(我对 SQLServer 和.NET 最感兴趣,但我也想了解其他平台如何解决这个问题。)

70548 次浏览

您将规范化您的数据库,这实际上将镜像您的继承。 它可能会导致性能下降,但正常化就是这样。你可能不得不使用良好的常识来找到平衡。

在数据库中可以设置两种主要的继承类型: 每个实体的表和每个层次结构的表。

表中有一个基实体表,该表具有所有子类的共享属性。然后,每个子类都有另一个表,每个表只有适用于该类的属性。他们的 PK 是1:1

alt text

每个层次结构中的表是所有类共享一个表的地方,可选属性为空。它们也是一个鉴别器字段,它是一个数字,表示记录当前持有的类型

alt text SessionTypeID 是鉴别器

每个层次结构的目标查询速度更快,因为您不需要连接(只需要识别器值) ,而每个实体的目标则需要执行复杂的连接,以便检测某些内容的类型并检索其所有数据。.

编辑: 我在这里展示的图片是我正在做的一个项目的屏幕截图。资产映像是不完整的,因此它是空的,但是它主要是显示它是如何设置的,而不是在表中放置什么。这取决于你;)。会话表包含虚拟协作会话信息,根据所涉及的协作类型,可以包含多种类型的会话。

在数据库中建模继承有几种方法。你选择哪个取决于你的需要。下面是一些选择:

按类别划分的表格(TPT)

每个类都有自己的表。基类包含所有的基类元素,每个从基类派生的类都有自己的表,主键也是基类表的外键; 派生表的类只包含不同的元素。

例如:

class Person {
public int ID;
public string FirstName;
public string LastName;
}


class Employee : Person {
public DateTime StartDate;
}

会产生以下表格:

table Person
------------
int id (PK)
string firstname
string lastname


table Employee
--------------
int id (PK, FK)
datetime startdate

按层次表(TPH)

有一个表表示所有继承层次结构,这意味着有几个列可能是稀疏的。添加了一个鉴别器列,告诉系统这是什么类型的行。

考虑到上面的类,您最终得到的是这个表:

table Person
------------
int id (PK)
int rowtype (0 = "Person", 1 = "Employee")
string firstname
string lastname
datetime startdate

对于行类型为0(Person)的任何行,起始日期始终为 null。

每混凝土表(TPC)

每个类都有自己的完全形式的表,没有对任何其他表的引用。

根据上面的类,最终得到的是这些表:

table Person
------------
int id (PK)
string firstname
string lastname


table Employee
--------------
int id (PK)
string firstname
string lastname
datetime startdate

相似线程回答的重复

在 O-R 映射中,继承映射到父表,其中父表和子表使用相同的标识符

比如说

create table Object (
Id int NOT NULL --primary key, auto-increment
Name varchar(32)
)
create table SubObject (
Id int NOT NULL  --primary key and also foreign key to Object
Description varchar(32)
)

SubObject 与 Object 有一个外键关系。创建子对象行时,必须首先创建一个对象行,并在两行中都使用 Id

编辑: 如果您还要查看模型行为,那么您需要一个 Type 表,它列出了表之间的继承关系,并指定了实现每个表行为的程序集和类名

看起来有点过了,但这取决于你想用它做什么!

使用 SQLALchemy (PythonORM) ,您可以进行两种类型的继承。

我的经验是使用一个单一表和一个判别列。例如,绵羊数据库(不开玩笑!)将所有绵羊存储在一个表中,公羊和母羊在该表中使用性别列进行处理。

因此,您可以查询所有的绵羊,并获取所有的绵羊。或者你可以只通过 Ram 查询,它只会得到 Ram。你也可以做一些事情,比如拥有一个只能是公羊的关系(例如,羊的父亲) ,等等。

注意,一些数据库引擎已经提供了类似于 邮差的本机继承机制。

例如,您可以像下面这样查询上面响应中描述的 Person/Employee 系统:

/* This shows the first name of all persons or employees */
SELECT firstname FROM Person ;


/* This shows the start date of all employees only */
SELECT startdate FROM Employee ;

在这是您的数据库的选择,您不需要特别聪明!

正确的数据库设计与正确的对象设计完全不同。

如果您计划将数据库用于除了简单地序列化对象之外的任何用途(例如报告、查询、多应用程序使用、业务智能等) ,那么我不推荐使用任何从对象到表的简单映射。

许多人认为数据库表中的一行是一个实体(我花了很多年来思考这个问题) ,但是一行并不是一个实体。这是个提议。数据库关系(即表)表示关于世界的一些事实陈述。行的存在表示事实为真(相反,没有行表示事实为假)。

通过这种理解,您可以看到面向对象程序中的单个类型可能存储在十几个不同的关系中。而且各种类型(通过继承、关联、聚合或完全独立而统一)可以部分存储在单个关系中。

最好问问你自己,你想要存储什么样的事实,你想要回答什么样的问题,你想要生成什么样的报告。

一旦创建了适当的 DB 设计,那么创建允许您将对象序列化到这些关系的查询/视图就很简单了。

例如:

在酒店预订系统中,你可能需要存储一个事实,那就是无名氏已经预订了 Seaview Inn 酒店4月10日至12日的房间。这是客户实体的属性吗?它是酒店实体的一个属性吗?它是一个包含客户和酒店的预订实体吗?它可以是面向对象系统中的任何或所有这些东西。在数据库中,这些都不是。这只是一个赤裸裸的事实。

要查看差异,请考虑以下两个查询。(1)无名氏明年预订了多少家酒店?(2)4月10日在 Seaview Inn 订了多少间房?

在面向对象系统中,query (1)是客户实体的属性,query (2)是酒店实体的属性。这些对象将在其 API 中公开这些属性。(不过,显然,获取这些值的内部机制可能涉及到对其他对象的引用。)

在一个关系数据库系统中,两个查询都会检查预订关系来获得它们的号码,而且从概念上来说,没有必要为任何其他“实体”而烦恼。

因此,正是通过试图存储关于世界的事实(而不是试图存储具有属性的实体) ,才构建了一个适当的关系数据库。一旦设计得当,就可以轻松构建在设计阶段做梦也想不到的有用查询,因为实现这些查询所需的所有事实都在合适的位置上。

简而言之,你不知道。

如果需要序列化对象,可以使用 ORM,或者更好的方法,比如 activerrecord 或盛行率。

如果需要存储数据,那么以关系方式存储数据(注意存储的内容,并注意 Jeffrey L Whitledge 刚才所说的内容) ,不要受到对象设计的影响。

正如 Brad Wilson 所提到的,TPT、 TPH 和 TPC 模式是你要走的路:

  • 从基类继承的子类可以被视为数据库中基类定义的弱实体,这意味着它们依赖于它们的基类,没有基类就不能存在。我曾多次看到,每个子表都存储了唯一的 ID,同时还保留了父表的 FK。一个 FK 就足够了,对于子表和基表之间的 FK 关系,启用 on-delete 级联就更好了。

  • 在 TPT 中,通过只查看基表记录,您无法找到该记录表示的子类。当您想要加载所有记录的列表时(不需要对每个子表执行 select ) ,有时需要这样做。处理这个问题的一种方法是用一列表示子类的类型(类似于 TPH 中的 rowType 字段) ,这样就可以以某种方式混合 TPT 和 TPH。

假设我们要设计一个数据库,其中包含以下形状类关系图:

public class Shape {
int id;
Color color;
Thickness thickness;
//other fields
}


public class Rectangle : Shape {
Point topLeft;
Point bottomRight;
}


public class Circle : Shape {
Point center;
int radius;
}

上述类的数据库设计可以如下:

table Shape
-----------
int id; (PK)
int color;
int thichkness;
int rowType; (0 = Rectangle, 1 = Circle, 2 = ...)


table Rectangle
----------
int ShapeID; (FK on delete cascade)
int topLeftX;
int topLeftY;
int bottomRightX;
int bottomRightY;


table Circle
----------
int ShapeID; (FK on delete cascade)
int centerX;
int center;
int radius;