Skip to main content
Version: 6.4

继承映射

映射超类

¥Mapped Superclasses

映射的超类是一个抽象或具体的类,它为其子类提供持久实体状态和映射信息,但它本身不是实体。通常,这种映射超类的目的是定义多个实体类共有的状态和映射信息。

¥A mapped superclass is an abstract or concrete class that provides persistent entity state and mapping information for its subclasses, but which is not itself an entity. Typically, the purpose of such a mapped superclass is to define state and mapping information that is common to multiple entity classes.

映射的超类,就像常规的非映射类一样,可以出现在其他映射继承层次结构的中间(通过单表继承)。

¥Mapped superclasses, just as regular, non-mapped classes, can appear in the middle of an otherwise mapped inheritance hierarchy (through Single Table Inheritance).

映射的超类不能是实体,它不可查询,并且由映射的超类定义的持久关系必须是单向的(仅具有拥有方)。这意味着在映射的超类上根本不可能进行一对多关联。此外,只有当映射的超类当前仅在一个实体中使用时,才有可能实现多对多关联。为了进一步支持继承,必须使用单表继承功能。

¥A mapped superclass cannot be an entity, it is not query-able and persistent relationships defined by a mapped superclass must be unidirectional (with an owning side only). This means that One-To-Many associations are not possible on a mapped superclass at all. Furthermore, Many-To-Many associations are only possible if the mapped superclass is only used in exactly one entity at the moment. For further support of inheritance, the single table inheritance features have to be used.

另请注意,我们不能使用泛型来定义任何关系。这意味着我们不能在基础实体中拥有通用类型参数,该参数将用作某些关系的目标。

¥Also note that we can't use generics to define any relations. This means that we cannot have a generic type argument in the base entity that would be used as a target of some relation.

// do not use @Entity decorator on base classes (mapped superclasses)
// we can also use @Entity({ abstract: true })
export abstract class Person {

@Property()
mapped1!: number;

@Property()
mapped2!: string;

@OneToOne()
toothbrush!: Toothbrush;

// ... more fields and methods
}

@Entity()
export class Employee extends Person {

@PrimaryKey()
id!: number;

@Property()
name!: string;

// ... more fields and methods

}

@Entity()
export class Toothbrush {

@PrimaryKey()
id!: number;

// ... more fields and methods

}

相应数据库模式的 DDL 看起来像这样(这是针对 SQLite 的):

¥The DDL for the corresponding database schema would look something like this (this is for SQLite):

create table `employee` (
`id` int unsigned not null auto_increment primary key,
`name` varchar(255) not null, `mapped1` integer not null,
`mapped2` varchar(255) not null,
`toothbrush_id` integer not null
);

从这个 DDL 片段中我们可以看到,实体子类只有一个表。映射超类的所有映射都被继承到子类,就好像它们是直接在该类上定义的一样。

¥As we can see from this DDL snippet, there is only a single table for the entity subclass. All the mappings from the mapped superclass were inherited to the subclass as if they had been defined on that class directly.

单表继承

¥Single Table Inheritance

版本 4.0 中添加了对 STI 的支持

¥Support for STI was added in version 4.0

单表继承 是一种继承映射策略,其中层次结构的所有类都映射到单个数据库表。为了区分哪一行代表层次结构中的哪种类型,使用了所谓的鉴别器列。

¥Single Table Inheritance is an inheritance mapping strategy where all classes of a hierarchy are mapped to a single database table. In order to distinguish which row represents which type in the hierarchy a so-called discriminator column is used.

@Entity({
discriminatorColumn: 'discr',
discriminatorMap: { person: 'Person', employee: 'Employee' },
})
export class Person {
// ...
}

@Entity()
export class Employee extends Person {
// ...
}

注意事项:

¥Things to note:

  • 必须在映射实体层次结构的最顶层类上指定 discriminatorColumn 选项。

    ¥The discriminatorColumn option must be specified on the topmost class that is part of the mapped entity hierarchy.

  • discriminatorMap 指定鉴别器列的哪些值将行标识为某种类型。在上述情况下,person 的值将一行标识为 Person 类型,employee 将一行标识为 Employee 类型。

    ¥The discriminatorMap specifies which values of the discriminator column identify a row as being of a certain type. In the case above a value of person identifies a row as being of type Person and employee identifies a row as being of type Employee.

  • 应在 discriminatorMap 中指定属于映射实体层次结构的所有实体类(包括最顶层的类)。在上述情况下,包括 Person 类。

    ¥All entity classes that are part of the mapped entity hierarchy (including the topmost class) should be specified in the discriminatorMap. In the case above Person class included.

  • 我们可以使用抽象类作为根实体 - 那么根类不应该是鉴别器映射的一部分

    ¥We can use abstract class as the root entity - then the root class should not be part of the discriminator map

  • 如果没有提供鉴别器映射,则会自动生成映射。自动生成的鉴别器映射包含表名,否则在常规实体的情况下会使用这些表名。

    ¥If no discriminator map is provided, then the map is generated automatically. The automatically generated discriminator map contains the table names that would be otherwise used in case of regular entities.

使用 discriminatorValue 而不是 discriminatorMap

¥Using discriminatorValue instead of discriminatorMap

如上所述,鉴别器映射可以自动生成。在这种情况下,我们可能想要控制将在映射中使用的令牌。为此,我们可以在子实体上使用 discriminatorValue

¥As noted above, the discriminator map can be auto-generated. In that case, we might want to control the tokens that will be used in the map. To do so, we can use discriminatorValue on the child entities:

@Entity({
discriminatorColumn: 'discr',
discriminatorValue: 'person',
})
export class Person {
// ...
}

@Entity({
discriminatorValue: 'employee',
})
export class Employee extends Person {
// ...
}

显式鉴别器列

¥Explicit discriminator column

discriminatorColumn 指定特殊列的名称,该列将用于定义应使用哪种类型的类来表示给定行。它将自动为我们定义,并且将保持隐藏状态(它不会作为常规属性被水化)。

¥The discriminatorColumn specifies the name of a special column that will be used to define what type of class a given row should be represented with. It will be defined automatically for us, and it will stay hidden (it won't be hydrated as a regular property).

另一方面,明确定义列是完全可以的。这样做,我们将能够:

¥On the other hand, it is perfectly fine to define the column explicitly. Doing so, we will be able to:

  • 按类型查询,例如 em.find(Person, { type: { $ne: 'employee' } }

    ¥querying by the type, e.g. em.find(Person, { type: { $ne: 'employee' } }

  • 该列将成为序列化响应的一部分

    ¥the column will be part of the serialized response

以下示例显示了如何明确定义鉴别器,以及根实体为抽象类的版本。

¥Following example shows how we can define the discriminator explicitly, as well as a version where root entity is abstract class.

@Entity({
discriminatorColumn: 'type',
discriminatorMap: { person: 'Person', employee: 'Employee' },
})
export abstract class BasePerson {

@Enum()
type!: 'person' | 'employee';

}

@Entity()
export class Person extends BasePerson {
// ...
}

@Entity()
export class Employee extends Person {
// ...
}

如果我们想将 discriminatorValue 与抽象实体一起使用,我们需要将实体标记为 abstract: true,以便可以从鉴别器映射中跳过它:

¥If we wanted to use discriminatorValue with abstract entities, we need to mark the entity as abstract: true so it can be skipped from the discriminator map:

@Entity({
discriminatorColumn: 'type',
abstract: true,
})
export abstract class BasePerson {

@Enum()
type!: 'person' | 'employee';

}

@Entity({ discriminatorValue: 'person' })
export class Person extends BasePerson {
// ...
}

@Entity({ discriminatorValue: 'employee' })
export class Employee extends Person {
// ...
}

设计时注意事项

¥Design-time considerations

当类型层次结构相当简单且稳定时,这种映射方法效果很好。向层次结构添加新类型并向现有超类型添加字段仅涉及向表中添加新列,但在大型部署中,这可能会对数据库内的索引和列布局产生不利影响。

¥This mapping approach works well when the type hierarchy is fairly simple and stable. Adding a new type to the hierarchy and adding fields to existing supertypes simply involves adding new columns to the table, though in large deployments this may have an adverse impact on the index and column layout inside the database.

性能影响

¥Performance impact

此策略对于跨层次结构中的所有类型或特定类型进行查询非常有效。不需要表连接,只需要一个列出类型标识符的 WHERE 子句。特别是,涉及采用这种映射策略的类型的关系非常高效。

¥This strategy is very efficient for querying across all types in the hierarchy or for specific types. No table joins are required, only a WHERE clause listing the type identifiers. In particular, relationships involving types that employ this mapping strategy are very performant.

SQL 模式注意事项

¥SQL Schema considerations

为了使单表继承在使用旧数据库模式或自写数据库模式的场景中工作,我们必须确保不在根实体中但在任何不同子实体中的所有列都必须允许空值。具有 NOT NULL 约束的列必须位于单表继承层次结构的根实体上。

¥For Single-Table-Inheritance to work in scenarios where we are using either a legacy database schema or a self-written database schema we have to make sure that all columns that are not in the root entity but in any of the different sub-entities has to allow null values. Columns that have NOT NULL constraints have to be on the root entity of the single-table inheritance hierarchy.

本文档的这一部分受到 doctrine 文档 的极大启发,因为这里的行为几乎相同。

¥This part of documentation is highly inspired by doctrine docs as the behaviour here is pretty much the same.