序列化
默认情况下,ORM 将在发现期间在所有实体原型上定义 toJSON
方法。这意味着当你尝试通过 JSON.stringify()
序列化实体时,ORM 序列化将自动启动。默认实现使用 EntityTransformer.toObject()
方法,将实体实例转换为 POJO。在此过程中,ORM 特定构造(如 Reference
或 Collection
封装器)将转换为其底层值。
¥By default, the ORM will define a toJSON
method on all of your entity prototypes during discovery. This means that when you try to serialize your entity via JSON.stringify()
, the ORM serialization will kick in automatically. The default implementation uses EntityTransformer.toObject()
method, which converts an entity instance into a POJO. During this process, ORM specific constructs like the Reference
or Collection
wrappers are converted to their underlying values.
隐藏属性
¥Hidden Properties
如果你想从序列化结果中省略某些属性,你可以在 @Property()
装饰器上使用 hidden
标志标记它们。要在类型级别提供此信息,你还需要使用 HiddenProps
符号:
¥If you want to omit some properties from serialized result, you can mark them with hidden
flag on @Property()
decorator. To have this information available on the type level, you also need to use the HiddenProps
symbol:
@Entity()
class Book {
// we use the `HiddenProps` symbol to define hidden properties on type level
[HiddenProps]?: 'hiddenField' | 'otherHiddenField';
@Property({ hidden: true })
hiddenField = Date.now();
@Property({ hidden: true, nullable: true })
otherHiddenField?: string;
}
const book = new Book(...);
console.log(wrap(book).toObject().hiddenField); // undefined
// @ts-expect-error accessing `hiddenField` will fail to compile thanks to the `HiddenProps` symbol
console.log(wrap(book).toJSON().hiddenField); // undefined
或者,你可以使用 Hidden
类型。它的工作方式与 Opt
类型(OptionalProps
符号的替代品)相同,并且可以以两种方式使用:
¥Alternatively, you can use the Hidden
type. It works the same as the Opt
type (an alternative for OptionalProps
symbol), and can be used in two ways:
-
使用泛型:
hiddenField?: Hidden<string>;
¥with generics:
hiddenField?: Hidden<string>;
-
使用交集:
hiddenField?: string & Hidden;
¥with intersections:
hiddenField?: string & Hidden;
两者的工作方式相同,并且可以与 HiddenProps
符号方法结合使用。
¥Both will work the same, and can be combined with the HiddenProps
symbol approach.
@Entity()
class Book {
@Property({ hidden: true })
hiddenField: Hidden<Date> = Date.now();
@Property({ hidden: true, nullable: true })
otherHiddenField?: string & Hidden;
}
影子属性
¥Shadow Properties
相反的情况是,你想要定义一个仅存在于内存中(不会持久保存到数据库中)的属性,可以通过将你的属性定义为 persist: false
来解决。此类属性可以通过 Entity.assign()
、em.create()
和 em.merge()
之一分配。它也将成为序列化结果的一部分。
¥The opposite situation where you want to define a property that lives only in memory (is not persisted into database) can be solved by defining your property as persist: false
. Such property can be assigned via one of Entity.assign()
, em.create()
and em.merge()
. It will be also part of serialized result.
这可以在处理通过 QueryBuilder
或 MongoDB 的聚合选择的附加值时处理。
¥This can be handled when dealing with additional values selected via QueryBuilder
or MongoDB's aggregations.
@Entity()
class Book {
@Property({ persist: false })
count?: number;
}
const book = new Book(...);
wrap(book).assign({ count: 123 });
console.log(wrap(book).toObject().count); // 123
console.log(wrap(book).toJSON().count); // 123
属性序列化器
¥Property Serializers
作为自定义 toJSON()
方法的替代,我们还可以使用属性序列化器。它们允许指定在序列化属性时将使用的回调:
¥As an alternative to custom toJSON()
method, we can also use property serializers. They allow to specify a callback that will be used when serializing a property:
@Entity()
class Book {
@ManyToOne({ serializer: value => value.name, serializedName: 'authorName' })
author: Author;
}
const author = new Author('God')
const book = new Book(author);
console.log(wrap(book).toJSON().authorName); // 'God'
隐式序列化
¥Implicit serialization
隐式序列化意味着在实体上调用 toObject()
或 toJSON()
,而不是明确使用 serialize()
辅助程序。自 v6 起,它完全基于 populate
提示工作。这意味着,除非你明确将某个实体标记为通过 wrap(entity).populated()
填充,否则只有当它是 populate
提示的一部分时,它才会成为序列化形式的一部分:
¥Implicit serialization means calling toObject()
or toJSON()
on the entity, as opposed to explicitly using the serialize()
helper. Since v6, it works entirely based on populate
hints. This means that, unless you explicitly marked some entity as populated via wrap(entity).populated()
, it will be part of the serialized form only if it was part of the populate
hint:
// let's say both Author and Book entity has a m:1 relation to Publisher entity
// we only populate the publisher relation of the Book entity
const user = await em.findOneOrFail(Author, 1, {
populate: ['books.publisher'],
});
const dto = wrap(user).toObject();
console.log(dto.publisher); // only the FK, e.g. `123`
console.log(dto.books[0].publisher); // populated, e.g. `{ id: 123, name: '...' }`
此外,隐式序列化现在也尊重部分加载提示。以前,所有加载的属性都是序列化的,部分加载仅在数据库查询级别上起作用。自 v6 起,我们还会在运行时修剪数据。这意味着除非该属性是部分加载提示(fields
选项)的一部分,否则它不会成为 DTO 的一部分。这里的主要区别是主键和外键,它们通常会被自动选择,因为它们是构建实体图所必需的,但不再是 DTO 的一部分。
¥Moreover, the implicit serialization now respects the partial loading hints too. Previously, all loaded properties were serialized, partial loading worked only on the database query level. Since v6, we also prune the data on runtime. This means that unless the property is part of the partial loading hint (fields
option), it won't be part of the DTO. Main difference here is the primary and foreign keys, that are often automatically selected as they are needed to build the entity graph, but will no longer be part of the DTO.
const user = await em.findOneOrFail(Author, 1, {
fields: ['books.publisher.name'],
});
const dto = wrap(user).toObject();
// only the publisher's name will be available + primary keys
// `{ id: 1, books: [{ id: 2, publisher: { id: 3, name: '...' } }] }`
这也适用于可嵌入对象,包括嵌套和对象模式。
¥This also works for embeddables, including nesting and object mode.
主键会自动包含在内。如果你想隐藏它们,你有两个选择:
¥Primary keys are automatically included. If you want to hide them, you have two options:
-
在属性选项中使用
hidden: true
¥use
hidden: true
in the property options -
在 ORM 配置中使用
serialization: { includePrimaryKeys: false }
¥use
serialization: { includePrimaryKeys: false }
in the ORM config
外键为 forceObject
¥Foreign keys are forceObject
如果你想强制执行对象,则未填充的关系将序列化为外键值,例如 { author: 1 }
,例如 { author: { id: 1 } }
,在你的 ORM 配置中使用 serialization: { forceObject: true }
。
¥Unpopulated relations are serialized as foreign key values, e.g. { author: 1 }
, if you want to enforce objects, e.g. { author: { id: 1 } }
, use serialization: { forceObject: true }
in your ORM config.
为了严格遵守全局配置选项的类型,你需要通过 Config
符号在实体类上定义它:
¥For strict typings to respect the global config option, you need to define it on your entity class via Config
symbol:
import { Config, Entity, ManyToOne, PrimaryKey, Ref, wrap } from '@mikro-orm/core';
@Entity()
class Book {
[Config]?: DefineConfig<{ forceObject: true }>;
@PrimaryKey()
id!: number;
@ManyToOne(() => User, { ref: true })
author!: Ref<User>;
}
const book = await em.findOneOrFail(Book, 1);
const dto = wrap(book).toObject();
const identityId = dto.author.id; // without the Config symbol, `dto.identity` would resolve to number
显式序列化
¥Explicit serialization
序列化过程通常由 populate
提示驱动。如果你想控制这一点,你可以使用 serialize()
助手:
¥The serialization process is normally driven by the populate
hints. If you want to take control over this, you can use the serialize()
helper:
import { serialize } from '@mikro-orm/core';
const dtos = serialize([user1, user2]);
// [
// { name: '...', books: [1, 2, 3], identity: 123 },
// { name: '...', ... },
// ]
const [dto] = serialize(user1); // always returns an array
// { name: '...', books: [1, 2, 3], identity: 123 }
// for a single entity instance we can as well use `wrap(e).serialize()`
const dto2 = wrap(user1).serialize();
// { name: '...', books: [1, 2, 3], identity: 123 }
默认情况下,每个关系都被视为未填充 - 这将导致外键值存在。加载的集合将表示为外键数组。为了控制序列化响应的形状,我们可以使用第二个 options
参数:
¥By default, every relation is considered as not populated - this will result in the foreign key values to be present. Loaded collections will be represented as arrays of the foreign keys. To control the shape of the serialized response we can use the second options
parameter:
interface SerializeOptions<T extends object, P extends string = never, E extends string = never> {
/** Specify which relation should be serialized as populated and which as a FK. */
populate?: AutoPath<T, P>[] | boolean;
/** Specify which properties should be omitted. */
exclude?: AutoPath<T, E>[];
/** Enforce unpopulated references to be returned as objects, e.g. `{ author: { id: 1 } }` instead of `{ author: 1 }`. */
forceObject?: boolean;
/** Ignore custom property serializers. */
ignoreSerializers?: boolean;
/** Skip properties with `null` value. */
skipNull?: boolean;
/** Only include properties for a specific group. If a property does not specify any group, it will be included, otherwise only properties with a matching group are included. */
groups?: string[];
}
这是一个更复杂的例子:
¥Here is a more complex example:
import { wrap } from '@mikro-orm/core';
const dto = wrap(author).serialize({
populate: ['books.author', 'books.publisher', 'favouriteBook'], // populate some relations
exclude: ['books.author.email'], // skip property of some relation
forceObject: true, // not populated or not initialized relations will result in object, e.g. `{ author: { id: 1 } }`
skipNull: true, // properties with `null` value won't be part of the result
});
如果你尝试填充未初始化的关系,它将具有与 forceObject
选项相同的效果 - 该值将表示为仅具有主键的对象。
¥If you try to populate a relation that is not initialized, it will have same effect as the forceObject
option - the value will be represented as object with just the primary key available.
序列化组
¥Serialization groups
每个属性都可以指定其序列化组,然后将其与显式序列化一起使用。
¥Every property can specify its serialization groups, which are then used with explicit serialization.
始终包含没有
groups
选项的属性。¥Properties without the
groups
option are always included.
让我们考虑以下实体:
¥Let's consider the following entity:
@Entity()
class User {
@PrimaryKey()
id!: number;
@Property()
username!: string;
@Property({ groups: ['public', 'private'] })
name!: string;
@Property({ groups: ['private'] })
email!: string;
}
现在当你调用 serialize()
时:
¥Now when you call serialize()
:
-
没有
groups
选项,你将获得所有属性¥without the
groups
option, you get all the properties -
使用
groups: ['public']
,你将获得id
、username
和name
属性¥with
groups: ['public']
you getid
,username
andname
properties -
使用
groups: ['private']
,你将获得id
、username
、name
和email
属性¥with
groups: ['private']
you getid
,username
,name
andemail
properties -
使用
groups: []
,你只能获得id
和username
属性(没有组的属性)¥with
groups: []
you get only theid
andusername
properties (those without groups)
const dto1 = serialize(user);
// User { id: 1, username: 'foo', name: 'Jon', email: 'jon@example.com' }
const dto2 = serialize(user, { groups: ['public'] });
// User { id: 1, username: 'foo', name: 'Jon' }
缓存和 toPOJO
¥Caching and toPOJO
虽然 toObject
和 serialize
通常足以序列化你的实体,但有一种用例经常会不足,那就是缓存。缓存实体时,通常希望忽略自定义序列化器或隐藏属性等内容。一旦你尝试从缓存中加载此实体,它就需要具有所有属性,就像你再次加载它一样。
¥While toObject
and serialize
are often enough for serializing your entities, there is one use case where they often fall short, which is caching. When caching an entity, you usually want to ignore things like custom serializers or hidden properties. Once you try to load this entity from cache, it needs to have all the properties just like if you load it again.
想象以下场景:你有一个 User
实体,该实体具有 password
属性,即 hidden: true
。调用 toObject()
或 serialize()
将省略此隐藏的 password
属性,而 toPOJO()
将保留它。如果你想要缓存这样的实体,你需要拥有所有属性,而不仅仅是那些可见的属性。
¥Imagine the following scenario: you have a User
entity that has a password
property, which is hidden: true
. Calling toObject()
or serialize()
would omit this hidden password
property, while toPOJO()
would keep it. If you want to cache such an entity, you want to have all the properties, not just those that are visible.
toPOJO
方法还将忽略序列化提示(populate
和fields
),并将扩展所有关系,除非它们形成循环。¥The
toPOJO
method will also ignore serialization hints (populate
andfields
) and will expand all relations unless they form a cycle.
自定义 toJSON
方法
¥Custom toJSON
method
你可以为 toJSON
提供自定义实现,同时使用 toObject
进行初始序列化:
¥You can provide custom implementation for toJSON
, while using toObject
for initial serialization:
@Entity()
class Book {
// ...
toJSON(strict = true, strip = ['id', 'email'], ...args: any[]): { [p: string]: any } {
const o = wrap(this, true).toObject(...args); // do not forget to pass rest params here
if (strict) {
strip.forEach(k => delete o[k]);
}
return o;
}
}
调用
toObject(...args)
时不要忘记传递其余参数,否则结果可能不稳定。¥Do not forget to pass rest params when calling
toObject(...args)
, otherwise the results might not be stable.