Skip to main content
Version: 6.4

类型安全关系

实体关系映射到实体引用 - 至少具有主键的实体实例。此引用存储在身份映射中,因此从数据库获取相同文档时,你将获得相同的对象引用。

¥Entity relations are mapped to entity references - instances of the entity that have at least the primary key available. This reference is stored in identity map, so you will get the same object reference when fetching the same document from database.

@ManyToOne(() => Author)
author!: Author; // the value is always instance of the `Author` entity

你可以检查实体是否通过 wrap(entity).isInitialized() 初始化,并使用 await wrap(entity).init() 对其进行初始化。这将触发数据库调用并填充实体,在身份映射中保留相同的引用。

¥You can check whether an entity is initialized via wrap(entity).isInitialized(), and use await wrap(entity).init() to initialize it. This will trigger database call and populate the entity, keeping the same reference in identity map.

const author = em.getReference(Author, 123);
console.log(author.id); // accessing the id will not trigger any db call
console.log(wrap(author).isInitialized()); // false
console.log(author.name); // undefined

await wrap(author).init(); // this will trigger db call
console.log(wrap(author).isInitialized()); // true
console.log(author.name); // defined

isInitialized() 方法可用于运行时检查,但最终可能会非常繁琐 - 我们可以做得更好!我们可以使用 Reference 封装器,而不必手动检查实体状态。

¥The isInitialized() method can be used for runtime checks, but that could end up being quite tedious - we can do better! Instead of manual checks for entity state, we can use the Reference wrapper.

Reference 封装器

¥Reference wrapper

当你在实体上定义 @ManyToOne@OneToOne 属性时,TypeScript 编译器会认为所需的实体始终被加载:

¥When you define @ManyToOne and @OneToOne properties on your entity, TypeScript compiler will think that desired entities are always loaded:

@Entity()
export class Book {

@PrimaryKey()
id!: number;

@ManyToOne()
author!: Author;

constructor(author: Author) {
this.author = author;
}

}

const book = await em.findOne(Book, 1);
console.log(book.author instanceof Author); // true
console.log(wrap(book.author).isInitialized()); // false
console.log(book.author.name); // undefined as `Author` is not loaded yet

你可以使用 Reference 封装器来解决这个问题。它只是封装实体,定义 load(): Promise<T> 方法,如果尚未可用,它将首先延迟加载关联。你还可以使用 unwrap(): T 方法访问底层实体而不加载它。

¥You can overcome this issue by using the Reference wrapper. It simply wraps the entity, defining load(): Promise<T> method that will first lazy load the association if not already available. You can also use unwrap(): T method to access the underlying entity without loading it.

你还可以使用 load<K extends keyof T>(prop: K): Promise<T[K]>,它的工作方式与 load() 类似,但返回指定的属性。

¥You can also use load<K extends keyof T>(prop: K): Promise<T[K]>, which works like load() but returns the specified property.

./entities/Book.ts
import { Entity, Ref, ManyToOne, PrimaryKey, ref } from '@mikro-orm/core';

@Entity()
export class Book {

@PrimaryKey()
id!: number;

@ManyToOne(() => Author, { ref: true })
author: Ref<Author>;

constructor(author: Author) {
this.author = ref(author);
}

}
const book1 = await em.findOne(Book, 1);
book.author instanceof Reference; // true
book1.author; // Ref<Author> (instance of `Reference` class)
book1.author.name; // type error, there is no `name` property
book1.author.unwrap().name; // unsafe sync access, undefined as author is not loaded
book1.author.isInitialized(); // false

const book2 = await em.findOne(Book, 1, { populate: ['author'] });
book2.author; // LoadedReference<Author> (instance of `Reference` class)
book2.author.$.name; // type-safe sync access

还有 getEntity()getProperty() 方法同步 getter,它将首先检查封装的实体是否已初始化,如果没有,它将抛出错误。

¥There are also getEntity() and getProperty() methods that are synchronous getters, that will first check if the wrapped entity is initialized, and if not, it will throw and error.

const book = await em.findOne(Book, 1);
console.log(book.author instanceof Reference); // true
console.log(wrap(book.author).isInitialized()); // false
console.log(book.author.getEntity()); // Error: Reference<Author> 123 not initialized
console.log(book.author.getProperty('name')); // Error: Reference<Author> 123 not initialized
console.log(await book.author.load('name')); // ok, loading the author first
console.log(book.author.getProperty('name')); // ok, author already loaded

如果你使用与 TsMorphMetadataProvider 不同的元数据提供程序(例如 ReflectMetadataProvider),你还需要明确设置 ref 参数:

¥If you use different metadata provider than TsMorphMetadataProvider (e.g. ReflectMetadataProvider), you will also need to explicitly set ref parameter:

@ManyToOne(() => Author, { ref: true })
author!: Ref<Author>;

使用 Reference.load()

¥Using Reference.load()

检索引用后,你可以利用异步 Reference.load() 方法加载完整实体。

¥After retrieving a reference, you can load the full entity by utilizing the asynchronous Reference.load() method.

const book1 = await em.findOne(Book, 1);
(await book1.author.load()).name; // async safe access

const book2 = await em.findOne(Book, 2);
const author = await book2.author.load();
author.name;
await book2.author.load(); // no additional query, already loaded

与始终刷新实体的 wrap(e).init() 相反,Reference.load() 方法仅在实体尚未加载到身份映射中时才查询数据库。

¥As opposed to wrap(e).init() which always refreshes the entity, Reference.load() method will query the database only if the entity is not already loaded in Identity Map.

ScalarReference 封装器

¥ScalarReference wrapper

Reference 封装器类似,我们也可以将带有 Ref 的惰性标量封装到 ScalarReference 对象中。这对于惰性标量属性很方便。

¥Similarly to the Reference wrapper, we can also wrap scalars with Ref into a ScalarReference object. This is handy for lazy scalar properties.

对于非对象类型,Ref 类型会自动解析为 ScalarReference,因此以下内容是正确的:

¥The Ref type automatically resolves to ScalarReference for non-object types, so the below is correct:

@Property({ lazy: true, ref: true })
passwordHash!: Ref<string>;
const user = await em.findOne(User, 1);
const passwordHash = await user.passwordHash.load();

对于类似对象的类型,如果你选择使用引用封装器,则应明确使用 ScalarRef<T> 类型。例如,你可能想要延迟加载一个大的 JSON 值:

¥For object-like types, if you choose to use the reference wrappers, you should use the ScalarRef<T> type explicitly. For example, you might want to lazily load a large JSON value:

@Property({ type: 'json', nullable: true, lazy: true, ref: true })
// ReportParameters is an object type, imagine it defined elsewhere.
reportParameters!: ScalarRef<ReportParameters | null>;

请记住,一旦通过 ScalarReference 管理标量值,通过 MikroORM 管理对象访问它将始终返回 ScalarReference 封装器。如果属性也是 nullable,这可能会造成混淆,因为 ScalarReference 永远是真实的。在这种情况下,你应该通过 ScalarReference<T> 的类型参数通知类型系统该属性的可空性,如上所示。下面是其工作原理的示例:

¥Keep in mind that once a scalar value is managed through a ScalarReference, accessing it through MikroORM managed objects will always return the ScalarReference wrapper. That can be confusing in case the property is also nullable, since the ScalarReference will always be truthy. In such cases, you should inform the type system of the nullability of the property through ScalarReference<T>'s type parameter as demonstrated above. Below is an example of how it all works:

// Say Report of id "1" has no reportParameters in the Database.
const report = await em.findOne(Report, 1);
if (report.reportParameters) {
// Logs Ref<?>, not the actual value. **Would always run***.
console.log(report.reportParameters);
//@ts-expect-error $/.get() is not available until the reference has been loaded.
// const mistake = report.reportParameters.$
}
const populatedReport = await em.populate(report, ['reportParameters']);
// Logs `null`
console.log(populatedReport.reportParameters.$);

Loaded 类型

¥Loaded type

如果你检查 em.findem.findOne 方法的返回类型,你可能会有点困惑 - 它们返回 Loaded 类型而不是实体:

¥If you check the return type of em.find and em.findOne methods, you might be a bit confused - instead of the entity, they return Loaded type:

// res1 is of type `Loaded<User, never>[]`
const res1 = await em.find(User, {});

// res2 is of type `Loaded<User, 'identity' | 'friends'>[]`
const res2 = await em.find(User, {}, { populate: ['identity', 'friends'] });

User 实体定义如下:

¥The User entity is defined as follows:

import { Entity, PrimaryKey, ManyToOne, OneToOne, Collection, Ref, ref } from '@mikro-orm/core';

@Entity()
export class User {

@PrimaryKey()
id!: number;

@ManyToOne(() => Identity)
identity: Ref<Identity>;

@ManyToMany(() => User)
friends = new Collection<User>(this);

constructor(identity: Identity) {
this.identity = ref(identity);
}

}

Loaded 类型将表示实体的哪些关系被填充,并将向它们添加一个特殊的 $ 符号,从而允许对已加载属性进行类型安全的同步访问。这与 Reference 封装器结合使用效果很好:

¥The Loaded type will represent what relations of the entity are populated, and will add a special $ symbol to them, allowing for type-safe synchronous access to the loaded properties. This works great in combination with the Reference wrapper:

如果你不喜欢像 $ 这样的魔法名称的符号,你也可以使用 get() 方法,它是它的别名。

¥If you don't like symbols with magic names like $, you can as well use the get() method, which is an alias for it.

// res is of type `Loaded<User, 'identity'>`
const user = await em.findOneOrFail(User, 1, { populate: ['identity'] });

// instead of the async `await user.identity.load()` call that would ensure the relation is loaded
// you can use the dynamically added `$` symbol for synchronous and type-safe access to it:
console.log(user.identity.$.email);

如果你省略 populate 提示,user 的类型将为 Loaded<User, never>user.identity.$ 符号将不可用 - 这样的调用会导致编译错误。

¥If you'd omit the populate hint, type of user would be Loaded<User, never> and the user.identity.$ symbol wouldn't be available - such call would end up with compilation error.

// if we try without the populate hint, the type is `Loaded<User, never>`
const user2 = await em.findOneOrFail(User, 2);

// TS2339: Property '$' does not exist on type '{ id: number; } & Reference'.
console.log(user.identity.$.email);

同样适用于 Collection 封装器,它提供运行时方法 isInitializedloadItemsinit,以及类型安全的 $ 符号。

¥Same works for the Collection wrapper, that offers runtime methods isInitialized, loadItems and init, as well as the type-safe $ symbol.

// res is of type `Loaded<User, 'friends'>`
const user = await em.findOneOrFail(User, 1, { populate: ['friends'] });

// instead of the async `await user.friends.loadItems()` call that would ensure the collection items are loaded
// you can use the dynamically added `$` symbol for synchronous and type-safe access to it:
for (const friend of user.friends.$) {
console.log(friend.email);
}

你还可以在自己的方法中使用 Loaded 类型,以在类型级别要求填充某些关系:

¥You can also use the Loaded type in your own methods, to require on type level that some relations will be populated:

function checkIdentity(user: Loaded<User, 'identity'>) {
if (!user.identity.$.email.includes('@')) {
throw new Error(`That's a weird e-mail!`);
}
}
// works
const u1 = await em.findOneOrFail(User, 2, { populate: ['identity'] });
checkIdentity(u1);

// fails
const u2 = await em.findOneOrFail(User, 2);
checkIdentity(u2);

请记住,这只是类型级别的信息,你可以通过类型断言轻松欺骗它。

¥Keep in mind this is all just a type-level information, you can easily trick it via type assertions.

分配给 Reference 属性

¥Assigning to Reference properties

当你将属性定义为 Reference 封装器时,你需要将 Reference 实例分配给它而不是实体。你可以通过 ref(entity) 将任何实体转换为 Reference 封装器,或者使用 em.getReference()wrapped 选项:

¥When you define the property as Reference wrapper, you will need to assign the Reference instance to it instead of the entity. You can convert any entity to a Reference wrapper via ref(entity), or use wrapped option of em.getReference():

ref(e)wrap(e).toReference() 的快捷方式,与 Reference.create(e) 相同。

¥ref(e) is a shortcut for wrap(e).toReference(), which is the same as Reference.create(e).

import { ref } from '@mikro-orm/core';

const book = await em.findOne(Book, 1);
const repo = em.getRepository(Author);

book.author = repo.getReference(2, { wrapped: true });

// same as:
book.author = ref(repo.getReference(2));
await em.flush();

自 v4.5 以来,可以通过环境变量设置大多数 ORM 选项。如果你想从实体构造函数内部创建引用,这会很方便:

¥Since v5 we can also create entity references without access to EntityManager. This can be handy if you want to create a reference from inside entity constructor:

import { Entity, ManyToOne, Rel, rel } from '@mikro-orm/core';

@Entity()
export class Book {

@ManyToOne(() => Author, { ref: true })
author!: Ref<Author>;

constructor(authorId: number) {
this.author = rel(Author, authorId);
}

}

另一种方法是使用作为 WrappedEntity 接口 一部分的 toReference() 方法:

¥Another way is to use toReference() method available as part of WrappedEntity interface:

const author = new Author(...)
book.author = wrap(author).toReference();

如果引用已经存在,则需要用新的 Reference 实例重新分配它 - 它们像实体一样拥有身份,因此你需要替换它们:

¥If the reference already exist, you need to re-assign it with a new Reference instance - they hold identity just like entities, so you need to replace them:

article.author = ref(new User(...));

什么是 Ref (IdentifiedReference)?

¥What is Ref (IdentifiedReference)?

Ref 是一种交叉类型,它将主键属性添加到 Reference 接口。它允许直接从 Reference 实例获取主键。

¥Ref is an intersection type that adds primary key property to the Reference interface. It allows to get the primary key from Reference instance directly.

默认情况下,我们尝试通过检查是否存在具有已知名称的属性来检测 PK。我们按顺序检查这些:_id, uuid, id - 通过 PrimaryKeyProp 符号([PrimaryKeyProp]?: 'foo';)手动设置属性名称。

¥By default, we try to detect the PK by checking if a property with a known name exists. We check for those in order: _id, uuid, id - with a way to manually set the property name via PrimaryKeyProp symbol ([PrimaryKeyProp]?: 'foo';).

我们还可以通过第二个泛型类型参数覆盖它。

¥We can also override this via second generic type argument.

const book = await em.findOne(Book, 1);
console.log(book.author.id); // ok, returns the PK

你还可以拥有非标准主键:

¥You can also have non-standard primary key:

author: Ref<Author, 'myPrimaryKey'>;

const book = await em.findOne(Book, 1);
console.log(book.author.myPrimaryKey); // ok, returns the PK

对于 MongoDB,将 PK 泛型类型参数定义为 'id' | '_id' 以访问 stringObjectId PK 值:

¥For MongoDB, define the PK generic type argument as 'id' | '_id' to access both string and ObjectId PK values:

./entities/Book.ts
@Entity()
export class Book {

@PrimaryKey()
_id!: ObjectId;

@SerializedPrimaryKey()
id!: string;

@ManyToOne(() => Author, { ref: true })
author!: Ref<Author, 'id' | '_id'>;

}
const book = await em.findOne(Book, 1);
console.log(book.author.id); // ok, returns string PK
console.log(book.author._id); // ok, returns ObjectId PK