复合和外键作为主键
版本 3.5 中添加了对复合键的支持
¥Support for composite keys was added in version 3.5
MikroORM 原生支持复合主键。复合键是一个非常强大的关系数据库概念,我们非常小心地确保 MikroORM 支持尽可能多的复合主键用例。MikroORM 支持原始数据类型的复合键以及外键作为主键。你还可以在关系中使用复合键实体。
¥MikroORM supports composite primary keys natively. Composite keys are a very powerful relational database concept, and we took good care to make sure MikroORM supports as many of the composite primary key use-cases. MikroORM supports composite keys of primitive data types as well as foreign keys as primary keys. You can also use your composite key entities in relationships.
本节介绍复合主键的语义如何工作以及它们如何映射到数据库。
¥This section shows how the semantics of composite primary keys work and how they map to the database.
一般注意事项
¥General Considerations
在调用 em.persist(entity)
之前,需要设置主键的值。
¥Primary keys need to have their values set before you call em.persist(entity)
.
仅原始类型
¥Primitive Types only
假设你想创建一个汽车数据库,并使用车型名称和生产年份作为主键:
¥Suppose you want to create a database of cars and use the model-name and year of production as primary keys:
@Entity()
export class Car {
@PrimaryKey()
name: string;
@PrimaryKey()
year: number;
// this is needed for proper type checks in `FilterQuery`
[PrimaryKeyProp]?: ['name', 'year'];
constructor(name: string, year: number) {
this.name = name;
this.year = year;
}
}
现在你可以使用这个实体:
¥Now you can use this entity:
const car = new Car('Audi A8', 2010);
await em.persist(car).flush();
对于查询,你需要在条件中提供所有主键或主键数组,其顺序与键的定义顺序相同:
¥And for querying you need to provide all primary keys in the condition or an array of primary keys in the same order as the keys were defined:
const audi1 = await em.findOneOrFail(Car, { name: 'Audi A8', year: 2010 });
const audi2 = await em.findOneOrFail(Car, ['Audi A8', 2010]);
如果你想使用第二种带有主键元组的方法,则需要通过
PrimaryKeyProp
符号指定实体主键的类型,如Car
实体中所示。¥If you want to use the second approach with primary key tuple, you will need to specify the type of entity's primary key via
PrimaryKeyProp
symbol as shown in theCar
entity.
当你的实体在以下属性名称之一下具有单个标量主键时,不需要
PrimaryKeyProp
:id: number | string | bigint
、_id: any
或uuid: string
。¥
PrimaryKeyProp
is not needed when your entity has single scalar primary key under one of following property names:id: number | string | bigint
,_id: any
oruuid: string
.
你还可以在关联中使用此实体。然后,MikroORM 将生成两个外键,一个用于名称,一个用于年份到相关实体。
¥You can also use this entity in associations. MikroORM will then generate two foreign keys one for name and to year to the related entities.
此示例展示了如何很好地解决 em.persist()
之前现有值的要求:通过将它们添加为构造函数的强制值。
¥This example shows how you can nicely solve the requirement for existing values before em.persist()
: By adding them as mandatory values for the constructor.
通过外部实体进行身份识别
¥Identity through foreign Entities
在大量用例中,实体的身份应由一个或多个父实体的实体确定。
¥There are tons of use-cases where the identity of an Entity should be determined by the entity of one or many parent entities.
-
实体的动态属性(例如
Article
)。每篇文章都有许多带有主键article_id
和attribute_name
的属性。¥Dynamic Attributes of an Entity (for example
Article
). Each Article has many attributes with primary keyarticle_id
andattribute_name
. -
Address
对象是Person
,地址的主键是user_id
。这不是复合主键的情况,但身份是通过外部实体和外键派生的。¥
Address
object of aPerson
, the primary key of the address isuser_id
. This is not a case of a composite primary key, but the identity is derived through a foreign entity and a foreign key. -
带有元数据的数据透视表可以建模为实体,例如两篇文章之间的连接,带有一些描述和分数。
¥Pivot Tables with metadata can be modelled as Entity, for example connections between two articles with a little description and a score.
通过外部实体映射身份的语义很简单:
¥The semantics of mapping identity through foreign entities are easy:
-
只允许在
@ManyToOnes
或@OneToOne
关联上使用。¥Only allowed on
@ManyToOnes
or@OneToOne
associations. -
在装饰器中使用
primary: true
。¥Use
primary: true
in the decorator.
用例 1:动态属性
¥Use-Case 1: Dynamic Attributes
我们继续使用具有任意属性的文章示例,映射如下所示:
¥We keep up the example of an Article with arbitrary attributes, the mapping looks like this:
@Entity()
export class Article {
@PrimaryKey()
id!: number;
@Property()
title!: string;
@OneToMany(() => ArticleAttribute, attr => attr.article, { cascade: Cascade.ALL })
attributes = new Collection<ArticleAttribute>(this);
}
@Entity()
export class ArticleAttribute {
@ManyToOne({ primary: true })
article: Article;
@PrimaryKey()
attribute: string;
@Property()
value!: string;
[PrimaryKeyProp]?: ['article', 'attribute']; // this is needed for proper type checks in `FilterQuery`
constructor(name: string, value: string, article: Article) {
this.attribute = name;
this.value = value;
this.article = article;
}
}
用例 2:简单派生身份
¥Use-Case 2: Simple Derived Identity
有时你要求两个对象通过 @OneToOne
关联相关,并且依赖类应该重新使用它所依赖的类的主键。一个很好的例子是用户地址关系:
¥Sometimes you have the requirement that two objects are related by a @OneToOne
association and that the dependent class should re-use the primary key of the class it depends on. One good example for this is a user-address relationship:
@Entity()
export class User {
@PrimaryKey()
id!: number;
@OneToOne(() => Address, address => address.user, { cascade: [Cascade.ALL] })
address?: Address; // virtual property (inverse side) to allow querying the relation
}
@Entity()
export class Address {
@OneToOne({ primary: true })
user!: User;
[PrimaryKeyProp]?: 'user'; // this is needed for proper type checks in `FilterQuery`
}
用例 3:使用元数据连接表
¥Use-Case 3: Join-Table with Metadata
在经典的订单产品商店示例中,有订单项目的概念,其中包含对订单和产品的引用以及其他数据,例如购买的产品数量,甚至当前价格。
¥In the classic order product shop example there is the concept of the order item which contains references to order and product and additional data such as the amount of products purchased and maybe even the current price.
@Entity()
export class Order {
@PrimaryKey()
id!: number;
@ManyToOne()
customer: Customer;
@OneToMany(() => OrderItem, item => item.order)
items = new Collection<OrderItem>(this);
@Property()
paid = false;
@Property()
shipped = false;
@Property()
created = new Date();
constructor(customer: Customer) {
this.customer = customer;
}
}
@Entity()
export class Product {
@PrimaryKey()
id!: number;
@Property()
name!: string;
@Property()
currentPrice!: number;
}
@Entity()
export class OrderItem {
@ManyToOne({ primary: true })
order: Order;
@ManyToOne({ primary: true })
product: Product;
@Property()
amount = 1;
@Property()
offeredPrice: number;
[PrimaryKeyProp]?: ['order', 'product']; // this is needed for proper type checks in `FilterQuery`
constructor(order: Order, product: Product, amount = 1) {
this.order = order;
this.product = product;
this.offeredPrice = product.currentPrice;
}
}
默认情况下,生成的数据透视表实体在后台用于表示数据透视表。从 v5.1 开始,我们可以通过 pivotEntity
选项提供自己的实现。
¥By default, a generated pivot table entity is used under the hood to represent the pivot table. Since v5.1 we can provide our own implementation via pivotEntity
option.
数据透视表实体需要恰好具有两个多对一属性,其中第一个属性需要指向拥有实体,第二个属性需要指向多对多关系的目标实体。
¥The pivot table entity needs to have exactly two many-to-one properties, where first one needs to point to the owning entity and the second to the target entity of the many-to-many relation.
@Entity()
export class Order {
@ManyToMany({ entity: () => Product, pivotEntity: () => OrderItem })
products = new Collection<Product>(this);
}
对于双向 M:N 关系,仅在拥有方指定 pivotEntity
选项就足够了。我们仍然需要通过 inversedBy
或 mappedBy
选项链接双方。
¥For bidirectional M:N relations, it is enough to specify the pivotEntity
option only on the owning side. We still need to link the sides via inversedBy
or mappedBy
option.
@Entity()
export class Product {
@ManyToMany({ entity: () => Order, mappedBy: o => o.products })
orders = new Collection<Order>(this);
}
如果我们想将新项目添加到这样的 M:N 集合中,我们需要拥有所有非 FK 属性来定义数据库级别的默认值。
¥If we want to add new items to such M:N collection, we need to have all non-FK properties to define a database level default value.
@Entity()
export class OrderItem {
@ManyToOne({ primary: true })
order: Order;
@ManyToOne({ primary: true })
product: Product;
@Property({ default: 1 })
amount!: number;
}
或者,我们可以直接使用 pivot 实体:
¥Alternatively, we can work with the pivot entity directly:
// create new item
const item = em.create(OrderItem, {
order: 123,
product: 321,
amount: 999,
});
await em.persist(item).flush();
// or remove an item via delete query
const em.nativeDelete(OrderItem, { order: 123, product: 321 });
我们也可以像前面的示例一样定义针对枢轴实体的 1:m 属性,并使用它来修改集合,同时使用 M:N 属性以便于阅读和过滤。
¥We can as well define the 1:m properties targeting the pivot entity as in the previous example, and use that for modifying the collection, while using the M:N property for easier reading and filtering purposes.
使用带复合键的 QueryBuilder
¥Using QueryBuilder with composite keys
在内部,复合键表示为元组,包含所有值,其顺序与主键的定义顺序相同。
¥Internally composite keys are represented as tuples, containing all the values in the same order as the primary keys were defined.
const qb1 = em.createQueryBuilder(CarOwner);
qb1.select('*').where({ car: { name: 'Audi A8', year: 2010 } });
console.log(qb1.getQuery()); // select `e0`.* from `car_owner` as `e0` where `e0`.`name` = ? and `e0`.`year` = ?
const qb2 = em.createQueryBuilder(CarOwner);
qb2.select('*').where({ car: ['Audi A8', 2010] });
console.log(qb2.getQuery()); // 'select `e0`.* from `car_owner` as `e0` where (`e0`.`car_name`, `e0`.`car_year`) = (?, ?)'
const qb3 = em.createQueryBuilder(CarOwner);
qb3.select('*').where({ car: [['Audi A8', 2010]] });
console.log(qb3.getQuery()); // 'select `e0`.* from `car_owner` as `e0` where (`e0`.`car_name`, `e0`.`car_year`) in ((?, ?))'
当你想要获取具有复合键的实体的引用时,这也适用:
¥This also applies when you want to get a reference to entity with composite key:
const ref = em.getReference(Car, ['Audi A8', 2010]);
console.log(ref instanceof Car); // true
本文档的这一部分受到 doctrine 教程 的极大启发,因为这里的行为几乎相同。
¥This part of documentation is highly inspired by doctrine tutorial as the behaviour here is pretty much the same.