第 5 章:类型安全
实体关系映射到实体引用 - 至少具有主键的实体实例。此引用存储在身份映射中,因此从数据库获取相同文档时,你将获得相同的对象引用。
¥Entity relations are mapped to entity references - instances of the entity that have at least the primary key available. This reference is stored in the Identity Map, so you will get the same object reference when fetching the same document from the database.
@ManyToOne(() => User)
author!: User; // the value is always instance of the `User` 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 lazily. This will trigger a database call and populate the entity, keeping the same reference in the Identity Map.
const user = em.getReference(User, 123);
console.log(user.id); // prints `123`, accessing the id will not trigger any db call
console.log(wrap(user).isInitialized()); // false, it's just a reference
console.log(user.name); // undefined
await wrap(user).init(); // this will trigger db call
console.log(wrap(user).isInitialized()); // true
console.log(user.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, the TypeScript compiler will think that the desired entities are always loaded:
@Entity()
export class Article {
@PrimaryKey()
id!: number;
@ManyToOne()
author!: User;
constructor(author: User) {
this.author = author;
}
}
const article = await em.findOne(Article, 1);
console.log(article.author instanceof User); // true
console.log(wrap(article.author).isInitialized()); // false
console.log(article.author.name); // undefined as `User` 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.
import { Entity, Ref, ManyToOne, PrimaryKey, Reference } from '@mikro-orm/core';
@Entity()
export class Article {
@PrimaryKey()
id!: number;
// This guide is using `ts-morph` metadata provider, so this is enough.
@ManyToOne()
author: Ref<User>;
constructor(author: User) {
this.author = ref(author);
}
}
const article1 = await em.findOne(Article, 1);
article.author instanceof Reference; // true
article1.author; // Ref<User> (instance of `Reference` class)
article1.author.name; // type error, there is no `name` property
article1.author.unwrap().name; // unsafe sync access, undefined as author is not loaded
article1.author.isInitialized(); // false
const article2 = await em.findOne(Article, 1, { populate: ['author'] });
article2.author; // LoadedReference<User> (instance of `Reference` class)
article2.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 article = await em.findOne(Article, 1);
console.log(article.author instanceof Reference); // true
console.log(wrap(article.author).isInitialized()); // false
console.log(article.author.getEntity()); // Error: Reference<User> 123 not initialized
console.log(article.author.getProperty('name')); // Error: Reference<User> 123 not initialized
console.log(await article.author.load('name')); // ok, loading the author first
console.log(article.author.getProperty('name')); // ok, author already loaded
如果你使用与 TsMorphMetadataProvider
不同的元数据提供程序(例如 ReflectMetadataProvider
),你还需要明确设置 ref
参数:
¥If you use a different metadata provider than TsMorphMetadataProvider
(e.g. ReflectMetadataProvider
), you will also need to explicitly set the ref
parameter:
@ManyToOne(() => User, { ref: true })
author!: Ref<User>;
使用 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 article1 = await em.findOne(Article, 1);
(await article1.author.load()).name; // async safe access
const article2 = await em.findOne(Article, 2);
const author = await article2.author.load();
author.name;
await article2.author.load(); // no additional query, already loaded
与始终刷新实体的
wrap(e).init()
相反,Reference.load()
方法仅在实体尚未加载到身份映射中时才查询数据库。¥As opposed to
wrap(e).init()
which always refreshes the entity, theReference.load()
method will query the database only if the entity is not already loaded in the 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.find
和 em.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 theget()
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, the type ofuser
would beLoaded<User, never>
and theuser.identity.$
symbol wouldn't be available - such call would end up with a 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
封装器,它提供运行时方法 isInitialized
、loadItems
和 init
,以及类型安全的 $
符号。
¥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 the wrapped
option of em.getReference()
:
ref(e)
是wrap(e).toReference()
的快捷方式,与Reference.create(e)
相同。¥
ref(e)
is a shortcut forwrap(e).toReference()
, which is the same asReference.create(e)
.
import { ref } from '@mikro-orm/core';
const article = await em.findOne(Article, 1);
const repo = em.getRepository(User);
article.author = repo.getReference(2, { wrapped: true });
// same as:
article.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 the entity constructor:
import { Entity, ManyToOne, Rel, rel } from '@mikro-orm/core';
@Entity()
export class Article {
@ManyToOne(() => User, { ref: true })
author!: Ref<User>;
constructor(authorId: number) {
this.author = rel(User, authorId);
}
}
另一种方法是使用作为 WrappedEntity
接口 一部分的 toReference()
方法:
¥Another way is to use toReference()
method available as part of the WrappedEntity
interface:
const author = new User(...)
article.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
类型?
¥What is Ref
type?
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 the PrimaryKeyProp
symbol ([PrimaryKeyProp]?: 'foo';
).
我们还可以通过第二个泛型类型参数覆盖它。
¥We can also override this via a second generic type argument.
const article = await em.findOne(Article, 1);
console.log(article.author.id); // ok, returns the PK
严格部分加载
¥Strict partial loading
Loaded
类型还尊重部分加载提示(fields
选项)。使用时,返回的类型将仅允许访问选定的属性。主键会自动选择并在类型级别可用。
¥The Loaded
type also respects the partial loading hints (fields
option). When used, the returned type will only allow accessing selected properties. Primary keys are automatically selected and available on the type level.
// article is typed to `Selected<Article, 'author', 'title' | 'author.email'>`
const article = await em.findOneOrFail(Article, 1, {
fields: ['title', 'author.email'],
populate: ['author'],
});
const id = article.id; // ok, PK is selected automatically
const title = article.title; // ok, title is selected
const publisher = article.publisher; // fail, not selected
const author = article.author.id; // ok, PK is selected automatically
const email = article.author.email; // ok, selected
const name = article.author.name; // fail, not selected
参见 实时演示:
¥See live demo: