Skip to main content
Version: 6.4

序列化

默认情况下,ORM 将在发现期间在所有实体原型上定义 toJSON 方法。这意味着当你尝试通过 JSON.stringify() 序列化实体时,ORM 序列化将自动启动。默认实现使用 EntityTransformer.toObject() 方法,将实体实例转换为 POJO。在此过程中,ORM 特定构造(如 ReferenceCollection 封装器)将转换为其底层值。

¥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'],你将获得 idusernamename 属性

    ¥with groups: ['public'] you get id, username and name properties

  • 使用 groups: ['private'],你将获得 idusernamenameemail 属性

    ¥with groups: ['private'] you get id, username, name and email properties

  • 使用 groups: [],你只能获得 idusername 属性(没有组的属性)

    ¥with groups: [] you get only the id and username 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

虽然 toObjectserialize 通常足以序列化你的实体,但有一种用例经常会不足,那就是缓存。缓存实体时,通常希望忽略自定义序列化器或隐藏属性等内容。一旦你尝试从缓存中加载此实体,它就需要具有所有属性,就像你再次加载它一样。

¥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 方法还将忽略序列化提示(populatefields),并将扩展所有关系,除非它们形成循环。

¥The toPOJO method will also ignore serialization hints (populate and fields) 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.