使用可嵌入项分离关注点
版本 4.0 中添加了对可嵌入项的支持
¥Support for embeddables was added in version 4.0
可嵌入类不是实体本身,而是嵌入在实体中并且可以查询的类。你主要希望使用它们来减少重复或分离关注点。值对象(例如日期范围或地址)是此功能的主要用例。
¥Embeddables are classes which are not entities themselves, but are embedded in entities and can also be queried. You'll mostly want to use them to reduce duplication or separating concerns. Value objects such as date range or address are the primary use case for this feature.
可嵌入类需要像常规实体一样被发现,在初始化 ORM 时不要忘记将它们添加到实体列表中。
¥Embeddables needs to be discovered just like regular entities, don't forget to add them to the list of entities when initializing the ORM.
可嵌入类可以包含具有基本 @Property()
映射的属性、嵌套的 @Embedded()
属性或 @Embedded()
属性数组。从 5.0 版开始,我们还可以使用 @ManyToOne()
属性。
¥Embeddables can contain properties with basic @Property()
mapping, nested @Embedded()
properties or arrays of @Embedded()
properties. From version 5.0 we can also use @ManyToOne()
properties.
出于本教程的目的,我们假设你的应用中有一个 User
类,并且你希望将地址存储在 User
类中。我们将把 Address
类建模为可嵌入的,而不是简单地将相应的列添加到 User
类。
¥For the purposes of this tutorial, we will assume that you have a User
class in your application, and you would like to store an address in the User
class. We will model the Address
class as an embeddable instead of simply adding the respective columns to the User
class.
- reflect-metadata
- ts-morph
- EntitySchema
import { Embeddable, Embedded, Entity, PrimaryKey, Property } from '@mikro-orm/core';
@Embeddable()
export class Address {
@Property()
street!: string;
@Property()
postalCode!: string;
@Property()
city!: string;
@Property()
country!: string;
}
@Entity()
export class User {
@PrimaryKey()
id!: number;
@Embedded(() => Address)
address!: Address;
}
import { Embeddable, Embedded, Entity, PrimaryKey, Property } from '@mikro-orm/core';
@Embeddable()
export class Address {
@Property()
street!: string;
@Property()
postalCode!: string;
@Property()
city!: string;
@Property()
country!: string;
}
@Entity()
export class User {
@PrimaryKey()
id!: number;
@Embedded()
address!: Address;
}
import { EntitySchema } from '@mikro-orm/core';
export class Address {
street!: string;
postalCode!: string;
city!: string;
country!: string;
}
export class User {
id!: number;
address!: Address;
}
export const UserSchema = new EntitySchema({
class: User,
properties: {
id: { primary: true, type: 'number' },
address: { kind: 'embedded', entity: 'Address' },
},
});
export const AddressSchema = new EntitySchema({
class: Address,
embeddable: true,
properties: {
street: { type: 'string' },
postalCode: { type: 'string' },
city: { type: 'string' },
country: { type: 'string' },
},
});
使用 ReflectMetadataProvider 时,你可能需要在装饰器选项中提供类:
@Embedded(() => Address)
或@Embedded({ entity: () => Address })
。¥When using ReflectMetadataProvider, you might need to provide the class in decorator options:
@Embedded(() => Address)
or@Embedded({ entity: () => Address })
.
就你的数据库模式而言,MikroORM 会自动将 Address
类中的所有列内联到 User
类的表中,就像你直接在那里声明它们一样。
¥In terms of your database schema, MikroORM will automatically inline all columns from the Address
class into the table of the User
class, just as if you had declared them directly there.
初始化可嵌入
¥Initializing embeddables
如果可嵌入对象中的所有字段都是可空的,你可能需要初始化可嵌入对象,以避免获取空值而不是嵌入对象。
¥In case all fields in the embeddable are nullable, you might want to initialize the embeddable, to avoid getting a null value instead of the embedded object.
- reflect-metadata
- ts-morph
- EntitySchema
@Embedded(() => Address)
address = new Address();
@Embedded()
address = new Address();
address: { kind: 'embedded', entity: 'Address', onCreate: () => new Address() },
列前缀
¥Column Prefixing
默认情况下,MikroORM 使用值对象名称为列添加前缀来命名列。
¥By default, MikroORM names your columns by prefixing them, using the value object name.
按照上述示例,你的列将被命名为 address_street
、address_postal_code
...
¥Following the example above, your columns would be named as address_street
, address_postal_code
...
你可以通过更改 @Embedded()
符号中的 prefix
属性来更改此行为以满足你的需求。
¥You can change this behaviour to meet your needs by changing the prefix
attribute in the @Embedded()
notation.
以下示例向你展示如何将前缀设置为 myPrefix_
:
¥The following example shows you how to set your prefix to myPrefix_
:
- reflect-metadata
- ts-morph
- EntitySchema
@Entity()
export class User {
@Embedded(() => Address, { prefix: 'myPrefix_' })
address!: Address;
}
@Entity()
export class User {
@Embedded({ prefix: 'myPrefix_' })
address!: Address;
}
address: { kind: 'embedded', entity: 'Address', prefix: 'myPrefix_' },
你还可以更精确地决定如何使用显式前缀确定列名。使用以下示例:
¥You can also decide more precisely how the column name is determined with an explicit prefix. With the example below:
-
absolute
模式(默认)在列的开头设置前缀,将其命名为addr_city
、addr_street
、...¥
absolute
mode (default) sets the prefix at the beginning of the column, naming themaddr_city
,addr_street
, ... -
relative
模式将前缀与其父级前缀(如果有)连接起来,将其命名为contact_addr2_city
、contact_addr2_street
、...¥
relative
mode concatenates the prefix with its parent's prefix (if any), naming themcontact_addr2_city
,contact_addr2_street
, ...
prefixMode
的默认值将在 v7 中更改为 relative
。
¥The default value of prefixMode
will change in v7 to relative
.
- reflect-metadata
- ts-morph
- EntitySchema
@Embeddable()
export class Contact {
@Embedded({ entity: () => Address, prefix: 'addr_', prefixMode: 'absolute' })
address!: Address;
@Embedded({ entity: () => Address, prefix: 'addr2_', prefixMode: 'relative' })
address2!: Address;
}
@Entity()
export class User {
@Embedded(() => Contact)
contact!: Contact;
}
@Embeddable()
export class Contact {
@Embedded({ prefix: 'addr_', prefixMode: 'absolute' })
address!: Address;
@Embedded({ prefix: 'addr2_', prefixMode: 'relative' })
address2!: Address;
}
@Entity()
export class User {
@Embedded()
contact!: Contact;
}
export class Contact {
address!: Address;
address2!: Address;
}
export class User {
id!: number;
contact!: Contact;
}
export const ContactSchema = new EntitySchema({
class: Contact,
embeddable: true,
properties: {
address: { kind: 'embedded', entity: 'Address', prefix: 'addr_', prefixMode: 'absolute' },
address2: { kind: 'embedded', entity: 'Address', prefix: 'addr2_', prefixMode: 'relative' },
},
});
export const UserSchema = new EntitySchema({
class: User,
properties: {
id: { primary: true, type: 'number' },
contact: { kind: 'embedded', entity: 'Contact' },
},
});
可以在 ORM 配置中定义默认行为:
¥The default behavior can be defined in the ORM configuration:
MikroORM.init({ embeddables: { prefixMode: 'absolute' } })
要让 MikroORM 删除前缀并直接使用值对象的属性名称,请设置 prefix: false
:
¥To have MikroORM drop the prefix and use the value object's property name directly, set prefix: false
:
- reflect-metadata
- ts-morph
- EntitySchema
@Embedded({ entity: () => Address, prefix: false })
address!: Address;
@Embedded({ prefix: false })
address!: Address;
address: { kind: 'embedded', entity: 'Address', prefix: false },
将可嵌入内容存储为对象
¥Storing embeddables as objects
从 MikroORM v4.2 开始,我们还可以将可嵌入项存储为对象,而不是将其属性内联到所属实体。
¥From MikroORM v4.2 we can also store the embeddable as an object instead of inlining its properties to the owing entity.
- reflect-metadata
- ts-morph
- EntitySchema
@Embedded({ entity: () => Address, object: true })
address!: Address;
@Embedded({ object: true })
address!: Address;
address: { kind: 'embedded', entity: 'Address', object: true },
在 SQL 驱动程序中,这将使用 JSON 列来存储值。
¥In SQL drivers, this will use a JSON column to store the value.
目前只有 MySQL 和 PostgreSQL 驱动程序支持通过 JSON 属性进行搜索。
¥Only MySQL and PostgreSQL drivers support searching by JSON properties currently.
本文档的这一部分受到 doctrine 教程 的极大启发,因为这里的行为几乎相同。
¥This part of documentation is highly inspired by doctrine tutorial as the behaviour here is pretty much the same.
数组可嵌入
¥Array of embeddables
嵌入式数组始终存储为 JSON。可以在嵌套的可嵌入对象中使用它们。
¥Embedded arrays are always stored as JSON. It is possible to use them inside nested embeddables.
- reflect-metadata
- ts-morph
- EntitySchema
@Embedded(() => Address, { array: true })
addresses: Address[] = [];
@Embedded()
addresses: Address[] = [];
address: { kind: 'embedded', entity: 'Address', onCreate: () => [], array: true },
嵌套可嵌入项
¥Nested embeddables
从 v4.4 开始,我们还可以嵌套可嵌入对象,包括内联模式和对象模式:
¥Starting with v4.4, we can also nest embeddables, both in inline mode and object mode:
- reflect-metadata
- ts-morph
- EntitySchema
import { Embeddable, Embedded, Entity, PrimaryKey, Property } from '@mikro-orm/core';
@Entity()
export class User {
@PrimaryKey()
id!: number;
@Property()
name!: string;
@Embedded(() => Profile, { object: true, nullable: true })
profile?: Profile;
}
@Embeddable()
export class Profile {
@Property()
username: string;
@Embedded(() => Identity)
identity: Identity;
constructor(username: string, identity: Identity) {
this.username = username;
this.identity = identity;
}
}
@Embeddable()
export class Identity {
@Property()
email: string;
constructor(email: string) {
this.email = email;
}
}
import { Embeddable, Embedded, Entity, PrimaryKey, Property } from '@mikro-orm/core';
@Entity()
export class User {
@PrimaryKey()
id!: number;
@Property()
name!: string;
@Embedded({ object: true })
profile?: Profile;
}
@Embeddable()
export class Profile {
@Property()
username: string;
@Embedded()
identity: Identity;
constructor(username: string, identity: Identity) {
this.username = username;
this.identity = identity;
}
}
@Embeddable()
export class Identity {
@Property()
email: string;
constructor(email: string) {
this.email = email;
}
}
import { EntitySchema } from '@mikro-orm/core';
export class User {
id!: number;
name!: string;
profile?: Profile;
}
export class Profile {
constructor(
public username: string,
public identity: Identity,
) {}
}
export class Identity {
constructor(public email: string) {}
}
export const UserSchema = new EntitySchema({
class: User,
properties: {
id: { primary: true, type: 'number' },
name: { type: 'string' },
address: { kind: 'embedded', entity: 'Address' },
},
});
export const ProfileSchema = new EntitySchema({
class: Profile,
embeddable: true,
properties: {
username: { type: 'string' },
identity: { kind: 'embedded', entity: 'Identity' },
},
});
export const IdentitySchema = new EntitySchema({
class: Identity,
embeddable: true,
properties: {
email: { type: 'string' },
},
});
多态可嵌入
¥Polymorphic embeddables
自 v5 起, 默认允许更新深度实体图。这意味着我们可以为单个嵌入属性定义多个类,并且将根据鉴别器列使用正确的类,类似于单表继承的工作方式。
¥Since v5, it is also possible to use polymorphic embeddables. This means we can define multiple classes for a single embedded property and the right one will be used based on the discriminator column, similar to how single table inheritance work.
- reflect-metadata
- ts-morph
- EntitySchema
import { Embeddable, Embedded, Entity, Enum, PrimaryKey, Property } from '@mikro-orm/core';
export enum AnimalType {
CAT,
DOG,
}
@Embeddable({ abstract: true, discriminatorColumn: 'type' })
export abstract class Animal {
@Enum(() => AnimalType)
type!: AnimalType;
@Property()
name!: string;
}
@Embeddable({ discriminatorValue: AnimalType.CAT })
export class Cat extends Animal {
@Property({ nullable: true })
canMeow?: boolean = true;
constructor(name: string) {
super();
this.type = AnimalType.CAT;
this.name = name;
}
}
@Embeddable({ discriminatorValue: AnimalType.DOG })
export class Dog extends Animal {
@Property({ nullable: true })
canBark?: boolean = true;
constructor(name: string) {
super();
this.type = AnimalType.DOG;
this.name = name;
}
}
@Entity()
export class Owner {
@PrimaryKey()
id!: number;
@Property()
name!: string;
@Embedded(() => [Cat, Dog])
pet!: Cat | Dog;
}
import { Embeddable, Embedded, Entity, Enum, PrimaryKey, Property } from '@mikro-orm/core';
export enum AnimalType {
CAT,
DOG,
}
@Embeddable({ abstract: true, discriminatorColumn: 'type' })
export abstract class Animal {
@Enum()
type!: AnimalType;
@Property()
name!: string;
}
@Embeddable({ discriminatorValue: AnimalType.CAT })
export class Cat extends Animal {
@Property()
canMeow? = true;
constructor(name: string) {
super();
this.type = AnimalType.CAT;
this.name = name;
}
}
@Embeddable({ discriminatorValue: AnimalType.DOG })
export class Dog extends Animal {
@Property()
canBark? = true;
constructor(name: string) {
super();
this.type = AnimalType.DOG;
this.name = name;
}
}
@Entity()
export class Owner {
@PrimaryKey()
id!: number;
@Property()
name!: string;
@Embedded()
pet!: Cat | Dog;
}
import { EntitySchema } from '@mikro-orm/core';
export enum AnimalType {
CAT,
DOG,
}
export abstract class Animal {
type!: AnimalType;
name!: string;
}
export class Cat extends Animal {
canMeow? = true;
constructor(name: string) {
super();
this.type = AnimalType.CAT;
this.name = name;
}
}
export class Dog extends Animal {
canBark? = true;
constructor(name: string) {
super();
this.type = AnimalType.DOG;
this.name = name;
}
}
export class Owner {
id!: number;
name!: string;
pet!: Cat | Dog;
}
export const AnimalSchema = new EntitySchema({
class: Animal,
embeddable: true,
abstract: true,
discriminatorColumn: 'type',
properties: {
username: { type: 'string' },
identity: { kind: 'embedded', entity: 'Identity' },
},
});
export const CatSchema = new EntitySchema({
class: Cat,
embeddable: true,
extends: 'Animal',
discriminatorValue: AnimalType.CAT,
properties: {
canMeow: { type: 'boolean', nullable: true },
},
});
export const DogSchema = new EntitySchema({
class: Dog,
embeddable: true,
extends: 'Animal',
discriminatorValue: AnimalType.DOG,
properties: {
canBark: { type: 'boolean', nullable: true },
},
});
export const OwnerSchema = new EntitySchema({
class: Owner,
properties: {
id: { primary: true, type: 'number' },
name: { type: 'string' },
pet: { kind: 'embedded', entity: 'Cat | Dog' },
},
});