使用实体管理器
持久化和刷新
¥Persist and Flush
我们应该首先描述 2 种方法来了解 MikroORM 中的持久化工作原理:em.persist()
和 em.flush()
。
¥There are 2 methods we should first describe to understand how persisting works in MikroORM: em.persist()
and em.flush()
.
em.persist(entity)
用于标记新实体以供将来持久化。它将使实体由给定的 EntityManager
管理,一旦调用 flush
,它将被写入数据库。
¥em.persist(entity)
is used to mark new entities for future persisting. It will make the entity managed by given EntityManager
and once flush
will be called, it will be written to the database.
要了解 flush
,让我们首先定义什么是托管实体:如果实体从数据库(通过 em.find()
、em.findOne()
或其他托管实体)获取或通过 em.persist()
注册为新实体,则该实体是托管实体。
¥To understand flush
, lets first define what managed entity is: An entity is managed if it’s fetched from the database (via em.find()
, em.findOne()
or via other managed entity) or registered as new through em.persist()
.
em.flush()
将遍历所有托管实体,计算适当的变更集并执行相应的数据库查询。由于从数据库加载的实体会自动进行管理,因此我们不必对它们调用 persist,并且 flush 足以更新它们。
¥em.flush()
will go through all managed entities, compute appropriate change sets and perform according database queries. As an entity loaded from database becomes managed automatically, we do not have to call persist on those, and flush is enough to update them.
const book = await em.findOne(Book, 1);
book.title = 'How to persist things...';
// no need to persist `book` as its already managed by the EM
await em.flush();
持久化和级联
¥Persisting and Cascading
要将实体状态保存到数据库,我们需要将其持久化。Persist 确定是否使用 insert
或 update
并计算适当的变更集。尚未持久化的实体引用(没有标识符)将自动级联持久化。
¥To save entity state to database, we need to persist it. Persist determines whether to use insert
or update
and computes appropriate change-set. Entity references that are not persisted yet (does not have identifier) will be cascade persisted automatically.
// use constructors in our entities for required parameters
const author = new Author('Jon Snow', 'snow@wall.st');
author.born = new Date();
const publisher = new Publisher('7K publisher');
const book1 = new Book('My Life on The Wall, part 1', author);
book1.publisher = publisher;
const book2 = new Book('My Life on The Wall, part 2', author);
book2.publisher = publisher;
const book3 = new Book('My Life on The Wall, part 3', author);
book3.publisher = publisher;
// just persist books, author and publisher will be automatically cascade persisted
await em.persist([book1, book2, book3]).flush();
// or one by one
em.persist(book1);
em.persist(book2);
em.persist(book3);
await em.flush(); // flush everything to database at once
实体引用
¥Entity references
MikroORM 将每个实体表示为一个对象,即使那些未完全加载的实体也是如此。这些被称为实体引用 - 它们实际上是常规实体类实例,但只有其主键可用。这使得无需查询数据库即可创建它们成为可能。引用像任何其他实体一样存储在身份映射中。
¥MikroORM represents every entity as an object, even those that are not fully loaded. Those are called entity references - they are in fact regular entity class instances, but only with their primary key available. This makes it possible to create them without querying the database. References are stored in the identity map just like any other entity.
const userRef = em.getReference(User, 1);
console.log(userRef);
这将记录类似 (User) { id: 1 }
的内容,注意类名被括在括号中 - 这告诉你实体处于未初始化状态,仅表示主键。
¥This will log something like (User) { id: 1 }
, note the class name being wrapped in parens - this tells you the entity is not-initialized state and represents just the primary key.
以下是你可以使用引用而不是完全加载的实体执行的常见操作的示例:
¥Here is an example of common actions you can do with a reference instead of a fully loaded entity:
// setting relation properties
author.favouriteBook = em.getReference(Book, 1);
// removing entity by reference
em.remove(em.getReference(Book, 2));
// adding entity to collection by reference
author.books.add(em.getReference(Book, 3));
该概念可以与所谓的 Reference
封装器相结合,以增加类型安全性,如 类型安全关系部分 中所述。
¥The concept can be combined with the so-called Reference
wrapper for added type safety as described in the Type-safe Relations section.
实体状态和 WrappedEntity
¥Entity state and WrappedEntity
在实体发现期间(当你调用 MikroORM.init()
时发生),ORM 将修补实体原型并为 WrappedEntity
生成一个惰性 getter - 包含有关实体的各种元数据和状态信息的类。每个实体实例都会有一个,在隐藏的 __helper
属性下可用 - 要以类型安全的方式访问其 API,请使用 wrap()
辅助程序:
¥During entity discovery (which happens when you call MikroORM.init()
), the ORM will patch the entity prototype and generate a lazy getter for the WrappedEntity
- a class holding various metadata and state information about the entity. Each entity instance will have one, available under a hidden __helper
property - to access its API in a type-safe way, use the wrap()
helper:
import { wrap } from '@mikro-orm/core';
const userRef = em.getReference(User, 1);
console.log('userRef is initialized:', wrap(userRef).isInitialized()); // false
await wrap(userRef).init();
console.log('userRef is initialized:', wrap(userRef).isInitialized()); // true
你还可以扩展 MikroORM 提供的
BaseEntity
。它定义了通过wrap()
助手可用的所有公共方法,因此你可以执行userRef.isInitialized()
或userRef.init()
。¥You can also extend the
BaseEntity
provided by MikroORM. It defines all the public methods available viawrap()
helper, so you could douserRef.isInitialized()
oruserRef.init()
.
WrappedEntity
实例还保存实体在加载或刷新时的状态 - 然后,工作单元在刷新期间使用此状态来计算差异。另一个用例是序列化,我们可以使用 toObject()
、toPOJO()
和 toJSON()
方法将实体实例转换为普通的 JavaScript 对象。
¥The WrappedEntity
instance also holds the state of the entity at the time it was loaded or flushed - this state is then used by the Unit of Work during flush to compute the differences. Another use case is serialization, we can use the toObject()
, toPOJO()
and toJSON()
methods to convert the entity instance to a plain JavaScript object.
删除实体
¥Removing entities
要通过 EntityManager
删除实体,我们有两种可能性:
¥To delete entities via EntityManager
, we have two possibilities:
-
通过
em.remove()
标记实体实例 - 这意味着我们首先需要拥有实体实例。但不要担心,即使不从数据库加载它,你也可以获得一个 - 通过em.getReference()
。¥Mark entity instance via
em.remove()
- this means we first need to have the entity instance. But don't worry, you can get one even without loading it from the database - viaem.getReference()
. -
通过
em.nativeDelete()
触发DELETE
查询 - 当你想要的只是一个简单的删除查询时,它可以很简单。¥Fire
DELETE
query viaem.nativeDelete()
- when all you want is a simple delete query, it can be simple as that.
让我们测试第一种方法,通过实体实例删除:
¥Let's test the first approach with removing by entity instance:
// using reference is enough, no need for a fully initialized entity
const book1 = em.getReference(Book, 1);
await em.remove(book1).flush();
使用 EntityManager 获取实体
¥Fetching Entities with EntityManager
要从数据库获取实体,我们可以使用 em.find()
和 em.findOne()
:
¥To fetch entities from database we can use em.find()
and em.findOne()
:
const author = await em.findOne(Author, 123);
const books = await em.find(Book, {});
for (const author of authors) {
console.log(author.name); // Jon Snow
for (const book of author.books) {
console.log(book.title); // initialized
console.log(book.author.isInitialized()); // true
console.log(book.author.id);
console.log(book.author.name); // Jon Snow
console.log(book.publisher); // just reference
console.log(book.publisher.isInitialized()); // false
console.log(book.publisher.id);
console.log(book.publisher.name); // undefined
}
}
或者,还有 em.findAll()
,它没有第二个 where
参数,默认返回所有实体。但是,你仍然可以使用此方法的 where
选项:
¥Alternatively, there is also em.findAll()
, which does not have the second where
parameter and defaults to returning all entities. You can still use the where
option of this method though:
const books = await em.findAll(Book, {
where: { publisher: { $ne: null } }, // optional
});
要填充实体关系,我们可以使用 populate
参数。
¥To populate entity relations, we can use populate
parameter.
const books = await em.findAll(Book, {
where: { publisher: { $ne: null } },
populate: ['author.friends'],
});
你还可以使用 em.populate()
助手在已加载的实体上填充关系(或确保它们已完全填充)。这在通过 QueryBuilder
加载实体时也很方便:
¥You can also use em.populate()
helper to populate relations (or to ensure they are fully populated) on already loaded entities. This is also handy when loading entities via QueryBuilder
:
const authors = await em.createQueryBuilder(Author).select('*').getResult();
await em.populate(authors, { populate: ['books.tags'] });
// now our Author entities will have `books` collections populated,
// as well as they will have their `tags` collections populated.
console.log(authors[0].books[0].tags[0]); // initialized BookTag
条件对象 (FilterQuery<T>
)
¥Conditions Object (FilterQuery<T>
)
通过条件对象(em.find(Entity, where: FilterQuery<T>)
中的 where
)查询实体支持多种不同的方式:
¥Querying entities via conditions object (where
in em.find(Entity, where: FilterQuery<T>)
) supports many different ways:
// search by entity properties
const users = await em.find(User, { firstName: 'John' });
// for searching by reference we can use primary key directly
const id = 1;
const users = await em.find(User, { organization: id });
// or pass unpopulated reference (including `Reference` wrapper)
const ref = await em.getReference(Organization, id);
const users = await em.find(User, { organization: ref });
// fully populated entities as also supported
const ent = await em.findOne(Organization, id);
const users = await em.find(User, { organization: ent });
// complex queries with operators
const users = await em.find(User, { $and: [{ id: { $nin: [3, 4] } }, { id: { $gt: 2 } }] });
// we can also search for array of primary keys directly
const users = await em.find(User, [1, 2, 3, 4, 5]);
// and in findOne all of this works, plus we can search by single primary key
const user1 = await em.findOne(User, 1);
从第五个例子中我们可以看到,还可以使用 $and
、$or
、$gte
、$gt
、$lte
、$lt
、$in
、$nin
、$eq
、$ne
、$like
、$re
和 $fulltext
等运算符。有关该内容的更多信息,请参阅 查询条件 部分。
¥As we can see in the fifth example, one can also use operators like $and
, $or
, $gte
, $gt
, $lte
, $lt
, $in
, $nin
, $eq
, $ne
, $like
, $re
and $fulltext
. More about that can be found in Query Conditions section.
在 FilterQuery
中使用自定义类
¥Using custom classes in FilterQuery
如果我们决定在自己的对象中抽象过滤选项,那么我们可能会遇到 find 选项无法返回我们期望的结果的问题。这是因为 FilterQuery
应该作为普通对象 (POJO) 提供,而不是具有原型的类实例。
¥If we decide to abstract the filter options in our own object then we might run into the problem that the find option does not return the results we'd expect. This is due to the fact that the FilterQuery
should be provided as a plain object (POJO), and not a class instance with prototype.
如果我们想提供自己的 FilterQuery
DTO,那么我们的 DTO 类应该扩展 PlainObject
类。这样,MikroORM 就知道应该这样对待它。
¥If we want to provide our own FilterQuery
DTO, then our DTO class should extend the PlainObject
class. This way MikroORM knows it should be treated as such.
import { PlainObject } from '@mikro-orm/core';
class Filter extends PlainObject {
name: string;
}
const where = new Filter();
where.name = 'Jon';
const res = await em.find(Author, where);
缓解 Type instantiation is excessively deep and possibly infinite.ts(2589)
错误
¥Mitigating Type instantiation is excessively deep and possibly infinite.ts(2589)
error
有时我们可能会遇到 TypeScript 错误,这是由于查询过于复杂而无法正确推断所有类型所致。通常可以通过明确提供类型参数来解决。
¥Sometimes we might be facing TypeScript errors caused by too complex query for it to properly infer all types. Usually it can be solved by providing the type argument explicitly.
你还可以选择使用存储库,因为类型推断不应该有问题。
¥You can also opt in to use repository instead, as there the type inference should not be problematic.
作为最后的手段,我们总是可以将查询类型转换为
any
。¥As a last resort, we can always type cast the query to
any
.
const books = await em.find<Book>(Book, { ... our complex query ... });
// or
const books = await em.getRepository(Book).find({ ... our complex query ... });
// or
const books = await em.find<any>(Book, { ... our complex query ... }) as Book[];
我们可能面临的另一个问题是 TypeScript 编译期间抛出的 RangeError: Maximum call stack size exceeded
错误(通常来自文件 node_modules/typescript/lib/typescript.js
)。解决方案是相同的,只需明确提供类型参数即可。
¥Another problem we might be facing is RangeError: Maximum call stack size exceeded
error thrown during TypeScript compilation (usually from file node_modules/typescript/lib/typescript.js
). The solution to this is the same, just provide the type argument explicitly.
按引用的实体字段搜索
¥Searching by referenced entity fields
你还可以按引用的实体属性进行搜索。只需将其安装在 Nest、MikroORM 和底层驱动程序旁边:目前,它只会将它们连接起来,以便我们可以通过它们进行搜索和排序。要填充实体,请不要忘记传递填充参数。
¥You can also search by referenced entity properties. Simply pass nested where condition like this and all requested relationships will be automatically joined. Currently, it will only join them so we can search and sort by those. To populate entities, do not forget to pass the populate parameter as well.
// find author of a book that has tag specified by name
const author = await em.findOne(Author, { books: { tags: { name: 'Tag name' } } });
console.log(author.books.isInitialized()); // false, as it only works for query and sort
const author = await em.findOne(Author, { books: { tags: { name: 'Tag name' } } }, { populate: ['books.tags'] });
console.log(author.books.isInitialized()); // true, because it was populated
console.log(author.books[0].tags.isInitialized()); // true, because it was populated
console.log(author.books[0].tags[0].isInitialized()); // true, because it was populated
此功能仅对 SQL 驱动程序完全可用。在 MongoDB 中,我们始终需要从拥有方进行查询 - 因此在上面的例子中,首先按名称加载书籍标签,然后是关联的书籍,然后是作者。另一个选项是反规范化模式。
¥This feature is fully available only for SQL drivers. In MongoDB always we need to query from the owning side - so in the example above, first load book tag by name, then associated book, then the author. Another option is to denormalize the schema.
部分加载
¥Partial loading
要仅获取某些数据库列,你可以使用 fields
选项:
¥To fetch only some database columns, you can use the fields
option:
const author = await em.findOne(Author, '...', {
fields: ['name', 'born'],
});
console.log(author.id); // PK is always selected
console.log(author.name); // Jon Snow
console.log(author.email); // undefined
这也适用于嵌套关系:
¥This works also for nested relations:
const author = await em.findOne(Author, '...', {
fields: ['name', 'books.title', 'books.author', 'books.price'],
});
即使你省略主键,也始终会选择它们。另一方面,你负责选择外键 - 如果你省略此类属性,则关系可能无法正确加载。在下面的例子中,书籍不会链接到作者,因为你没有指定要加载的 books.author
字段。
¥Primary keys are always selected even if you omit them. On the other hand, you are responsible for selecting the foreign keys—if you omit such property, the relation might not be loaded properly. In the following example the books would not be linked the author, because you did not specify the books.author
field to be loaded.
// this will load both author and book entities, but they won't be connected due to the missing FK in select
const author = await em.findOne(Author, '...', {
fields: ['name', 'books.title', 'books.price'],
});
同样的问题可能发生在 mongo 中,M:N 集合 — 它们作为数组属性存储在拥有实体上,因此你也需要确保标记此类属性。
¥The Same problem can occur in mongo with M:N collections—those are stored as array property on the owning entity, so you need to make sure to mark such properties too.
const author = await em.findOne(Author, '...', {
fields: ['name', 'books.title', 'books.author', 'books.price'],
});
或者,你可以使用 exclude
选项,它将省略提供的属性并选择其他所有内容:
¥Alternatively, you can use the exclude
option, which will omit the provided properties and select everything else:
const author = await em.findOne(Author, '...', {
exclude: ['email', 'books.price'],
populate: ['books'], // unlike with `fields`, you need to explicitly populate the relation here
});
获取分页结果
¥Fetching Paginated Results
如果我们要对结果进行分页,我们可以使用 em.findAndCount()
,它将在应用限制和偏移之前返回实体总数。
¥If we are going to paginate our results, we can use em.findAndCount()
that will return total count of entities before applying limit and offset.
const [authors, count] = await em.findAndCount(Author, { ... }, { limit: 10, offset: 50 });
console.log(authors.length); // based on limit parameter, e.g. 10
console.log(count); // total count, e.g. 1327
基于游标的分页
¥Cursor-based pagination
作为使用 limit
和 offset
的基于偏移量的分页的替代,我们可以基于游标进行分页。光标是一个不透明的字符串,它定义有序实体图中的特定位置。你可以使用 em.findByCursor()
访问这些选项。在底层,它将像 em.findAndCount()
方法一样调用 em.find()
和 em.count()
,但将使用游标选项。
¥As an alternative to the offset based pagination with limit
and offset
, we can paginate based on a cursor. A cursor is an opaque string that defines specific place in ordered entity graph. You can use em.findByCursor()
to access those options. Under the hood, it will call em.find()
and em.count()
just like the em.findAndCount()
method, but will use the cursor options instead.
支持 before
、after
、first
和 last
选项,同时不允许 limit
和 offset
。需要显式 orderBy
选项。
¥Supports before
, after
, first
and last
options while disallowing limit
and offset
. Explicit orderBy
option is required.
使用 first
和 after
进行正向分页,或使用 last
和 before
进行反向分页。
¥Use first
and after
for forward pagination, or last
and before
for backward pagination.
-
first
和last
是数字,可作为offset
的替代方案,这些选项是互斥的,一次只能使用一个¥
first
andlast
are numbers and serve as an alternative tooffset
, those options are mutually exclusive, use only one at a time -
before
和after
指定前一个游标值,可以是以下之一:¥
before
andafter
specify the previous cursor value, it can be one of the:-
Cursor
实例¥
Cursor
instance -
startCursor/endCursor
属性提供的不透明字符串¥opaque string provided by
startCursor/endCursor
properties -
POJO/实体实例
¥POJO/entity instance
-
const currentCursor = await em.findByCursor(User, {}, {
first: 10,
after: previousCursor, // cursor instance
orderBy: { id: 'desc' },
});
// to fetch next page
const nextCursor = await em.findByCursor(User, {}, {
first: 10,
after: currentCursor.endCursor, // opaque string
orderBy: { id: 'desc' },
});
// to fetch next page
const nextCursor2 = await em.findByCursor(User, {}, {
first: 10,
after: { id: lastSeenId }, // entity-like POJO
orderBy: { id: 'desc' },
});
Cursor
对象提供以下接口:
¥The Cursor
object provides following interface:
Cursor<User> {
items: [
User { ... },
User { ... },
User { ... },
...
],
totalCount: 50,
length: 10,
startCursor: 'WzRd',
endCursor: 'WzZd',
hasPrevPage: true,
hasNextPage: true,
}
处理未找到的实体
¥Handling Not Found Entities
当我们调用 em.findOne()
并且根据我们的标准未找到任何实体时,将返回 null
。如果我们宁愿抛出一个 Error
实例,我们可以使用 em.findOneOrFail()
:
¥When we call em.findOne()
and no entity is found based on our criteria, null
will be returned. If we rather have an Error
instance thrown, we can use em.findOneOrFail()
:
const author = await em.findOne(Author, { name: 'does-not-exist' });
console.log(author === null); // true
try {
const author = await em.findOneOrFail(Author, { name: 'does-not-exist' });
// author will be always found here
} catch (e) {
console.error('Not found', e);
}
你可以通过 findOneOrFailHandler
选项全局自定义错误,也可以通过 findOneOrFail
调用中的 failHandler
选项本地自定义错误。
¥You can customize the error either globally via findOneOrFailHandler
option, or locally via failHandler
option in findOneOrFail
call.
try {
const author = await em.findOneOrFail(Author, { name: 'does-not-exist' }, {
failHandler: (entityName: string, where: Record<string, any> | IPrimaryKey) => new Error(`Failed: ${entityName} in ${util.inspect(where)}`)
});
} catch (e) {
console.error(e); // our custom error
}
使用自定义 SQL 片段
¥Using custom SQL fragments
WHERE
查询或 ORDER BY
子句中的任何 SQL 片段都需要用 raw()
或 sql
封装:
¥Any SQL fragment in your WHERE
query or ORDER BY
clause need to be wrapped with raw()
or sql
:
const users = await em.find(User, { [sql`lower(email)`]: 'foo@bar.baz' }, {
orderBy: { [sql`(point(loc_latitude, loc_longitude) <@> point(0, 0))`]: 'ASC' },
});
这将产生以下查询:
¥This will produce following query:
select `e0`.*
from `user` as `e0`
where lower(email) = 'foo@bar.baz'
order by (point(loc_latitude, loc_longitude) <@> point(0, 0)) asc
在 使用原始 SQL 查询片段 部分中阅读有关此内容的更多信息。
¥Read more about this in Using raw SQL query fragments section.
更新引用(未加载实体)
¥Updating references (not loaded entities)
从 v5.5 开始,我们可以通过工作单元更新引用,就像它是一个加载的实体一样。这样就可以在不加载实体的情况下发出更新查询。
¥Since v5.5, we can update references via Unit of Work, just like if it was a loaded entity. This way it is possible to issue update queries without loading the entity.
const ref = em.getReference(Author, 123);
ref.name = 'new name';
ref.email = 'new email';
await em.flush();
这大致相当于调用 em.nativeUpdate()
,但有一个显着的区别 - 我们使用处理事件执行的刷新操作,因此将触发所有生命周期钩子以及刷新事件。
¥This is a rough equivalent to calling em.nativeUpdate()
, with one significant difference - we use the flush operation which handles event execution, so all life cycle hooks as well as flush events will be fired.
通过 raw()
助手进行原子更新
¥Atomic updates via raw()
helper
当你想通过 flush 发出原子更新查询时,可以使用静态 raw()
助手:
¥When you want to issue an atomic update query via flush, you can use the static raw()
helper:
const ref = em.getReference(Author, 123);
ref.age = raw(`age * 2`);
await em.flush();
console.log(ref.age); // real value is available after flush
raw()
助手返回特殊的原始查询片段对象。它不允许序列化(通过 toJSON
)以及使用值(通过 valueOf()
)。只允许使用此值一次,如果你尝试将其重新分配给另一个实体,则会引发错误以保护你免受以下错误的影响:
¥The raw()
helper returns special raw query fragment object. It disallows serialization (via toJSON
) as well as working with the value (via valueOf()
). Only single use of this value is allowed, if you try to reassign it to another entity, an error will be thrown to protect you from mistakes like this:
order.number = raw(`(select max(num) + 1 from orders)`);
user.lastOrderNumber = order.number; // throws, it could resolve to a different value
JSON.stringify(order); // throws, raw value cannot be serialized
Upsert
我们可以使用 em.upsert()
创建或更新实体,具体取决于它是否已经存在于数据库中。此方法执行 insert on conflict merge
查询,确保数据库同步,并返回托管实体实例。该方法可以接受 entityName
和实体 data
,也可以只接受实体实例。
¥We can use em.upsert()
create or update the entity, based on whether it is already present in the database. This method performs an insert on conflict merge
query ensuring the database is in sync, returning a managed entity instance. The method accepts either entityName
together with the entity data
, or just entity instance.
// insert into "author" ("age", "email") values (33, 'foo@bar.com') on conflict ("email") do update set "age" = 33
const author = await em.upsert(Author, { email: 'foo@bar.com', age: 33 });
实体数据需要包含主键或任何其他唯一属性。让我们考虑以下示例,其中 Author.email
是一个唯一属性:
¥The entity data needs to contain either the primary key, or any other unique property. Let's consider the following example, where Author.email
is a unique property:
// insert into "author" ("age", "email") values (33, 'foo@bar.com') on conflict ("email") do update set "age" = 33
// select "id" from "author" where "email" = 'foo@bar.com'
const author = await em.upsert(Author, { email: 'foo@bar.com', age: 33 });
根据驱动程序支持,这将使用返回查询或单独的选择查询来获取主键(如果 data
中缺少主键)。
¥Depending on the driver support, this will either use a returning query, or a separate select query, to fetch the primary key if it's missing from the data
.
你还可以使用分离的实体实例,在 em.upsert()
调用之后它将变为托管的。
¥You can also use detached entity instance, after the em.upsert()
call it will become managed.
const author = em.create(Author, { email: 'foo@bar.com', age: 33 });
await em.upsert(author);
从 v5.6 开始,也有具有类似签名的 em.upsertMany()
:
¥Since v5.6 there is also em.upsertMany()
with similar signature:
const [author1, author2, author3] = await em.upsertMany(Author, [
{ email: 'a1', age: 41 },
{ email: 'a2', age: 42 },
{ email: 'a3', age: 43 },
]);
默认情况下,EntityManager 将优先使用主键,并回退到具有值的第一个唯一属性。有时这可能不是想要的行为,一个例子是当你通过属性初始化器生成主键时,例如使用 uuid.v4()
。对于那些高级情况,你可以通过以下选项控制底层更新逻辑的工作方式:
¥By default, the EntityManager will prefer using the primary key, and fallback to the first unique property with a value. Sometimes this might not be the wanted behaviour, one example is when you generate the primary key via property initializer, e.g. with uuid.v4()
. For those advanced cases, you can control how the underlying upserting logic works via the following options:
-
onConflictFields?: (keyof T)[]
控制冲突子句¥
onConflictFields?: (keyof T)[]
to control the conflict clause -
onConflictAction?: 'ignore' | 'merge'
使用忽略和合并,因为这就是 QB 方法的调用方式¥
onConflictAction?: 'ignore' | 'merge'
used ignore and merge as that is how the QB methods are called -
onConflictMergeFields?: (keyof T)[]
控制合并子句¥
onConflictMergeFields?: (keyof T)[]
to control the merge clause -
onConflictExcludeFields?: (keyof T)[]
从合并子句中省略字段¥
onConflictExcludeFields?: (keyof T)[]
to omit fields from the merge clause
const [author1, author2, author3] = await em.upsertMany(Author, [{ ... }, { ... }, { ... }], {
onConflictFields: ['email'], // specify a manual set of fields pass to the on conflict clause
onConflictAction: 'merge',
onConflictExcludeFields: ['id'],
});
这将生成类似于以下内容的查询:
¥This will generate query similar to the following:
insert into "author"
("id", "current_age", "email", "foo")
values
(1, 41, 'a1', true),
(2, 42, 'a2', true),
(5, 43, 'a3', true)
on conflict ("email")
do update set
"current_age" = excluded."current_age",
"foo" = excluded."foo"
returning "_id", "current_age", "foo", "bar"
刷新实体状态
¥Refreshing entity state
我们可以使用 em.refresh(entity)
将实体状态与数据库同步。这是使用 refresh: true
调用 em.findOne()
并禁用自动刷新的快捷方式。
¥We can use em.refresh(entity)
to synchronize the entity state with database. This is a shortcut for calling em.findOne()
with refresh: true
and disabled auto-flush.
这导致对该实体所做的任何更改丢失。
¥This results in loss of any changes done to that entity.
const author = await em.findOneOrFail(Author, { name: 'Jon' });
console.log(author.name); // 'Jon'
// changes to entity will be lost!
author.name = '123';
// refresh the value, ignore any changes
await em.refresh(author);
console.log(author.name); // 'Jon'
批量插入、更新和删除
¥Batch inserts, updates and deletes
当你刷新对一个实体类型所做的更改时,每个给定操作(创建/更新/删除)只会执行一个查询。
¥When you flush changes made to one entity type, only one query per given operation (create/update/delete) will be executed.
for (let i = 1; i <= 5; i++) {
const u = new User(`Peter ${i}`, `peter+${i}@foo.bar`);
em.persist(u);
}
await em.flush();
// insert into `user` (`name`, `email`) values
// ('Peter 1', 'peter+1@foo.bar'),
// ('Peter 2', 'peter+2@foo.bar'),
// ('Peter 3', 'peter+3@foo.bar'),
// ('Peter 4', 'peter+4@foo.bar'),
// ('Peter 5', 'peter+5@foo.bar');
for (const user of users) {
user.name += ' changed!';
}
await em.flush();
// update `user` set
// `name` = case
// when (`id` = 1) then 'Peter 1 changed!'
// when (`id` = 2) then 'Peter 2 changed!'
// when (`id` = 3) then 'Peter 3 changed!'
// when (`id` = 4) then 'Peter 4 changed!'
// when (`id` = 5) then 'Peter 5 changed!'
// else `priority` end
// where `id` in (1, 2, 3, 4, 5)
em.remove(users);
await em.flush();
// delete from `user` where `id` in (1, 2, 3, 4, 5)
禁用身份映射和更改集跟踪
¥Disabling identity map and change set tracking
有时我们可能想要禁用身份映射并更改某些查询的集合跟踪。这可以通过 disableIdentityMap
选项实现。在幕后,它将创建新的上下文,加载其中的实体,然后清除它,因此主身份图将保持干净。
¥Sometimes we might want to disable identity map and change set tracking for some query. This is possible via disableIdentityMap
option. Behind the scenes, it will create new context, load the entities inside that, and clear it afterwards, so the main identity map will stay clean.
与托管实体相反,此类实体称为分离实体。为了能够使用它们,我们首先需要通过
em.merge()
合并它们。¥As opposed to managed entities, such entities are called detached. To be able to work with them, we first need to merge them via
em.merge()
.
const users = await em.find(User, { email: 'foo@bar.baz' }, {
disableIdentityMap: true,
populate: ['cars.brand'],
});
users[0].name = 'changed';
await em.flush(); // calling flush have no effect, as the entity is not managed
请记住,这也可能具有 对性能的负面影响。
¥Keep in mind that this can also have negative effect on the performance.
实体存储库
¥Entity Repositories
尽管我们可以直接使用 EntityManager
,但更方便的方法是使用 代替 EntityRepository
。你可以在依赖注入容器(如 InversifyJS)中注册你的存储库,这样我们就不需要每次都从 EntityManager
获取它们。
¥Although we can use EntityManager
directly, a more convenient way is to use EntityRepository
instead. You can register your repositories in dependency injection container like InversifyJS so we do not need to get them from EntityManager
each time.
有关更多示例,请查看 tests/EntityManager.mongo.test.ts
或 tests/EntityManager.mysql.test.ts
。
¥For more examples, take a look at tests/EntityManager.mongo.test.ts
or tests/EntityManager.mysql.test.ts
.
自定义属性排序
¥Custom Property Ordering
实体属性通过 customOrder
属性提供一些自定义排序支持。这对于具有与其底层数据表示不一致的自然顺序的值很有用。考虑下面的代码,自然排序顺序为 high
、low
、medium
。但是,我们可以提供 customOrder
来指示枚举值的排序方式。
¥Entity properties provide some support for custom ordering via the customOrder
attribute. This is useful for values that have a natural order that doesn't align with their underlying data representation. Consider the code below, the natural sorting order would be high
, low
, medium
. However, we can provide the customOrder
to indicate how the enum values should be sorted.
enum Priority {
Low = 'low',
Medium = 'medium',
High = 'high',
}
@Entity()
class Task {
@PrimaryKey()
id!: number
@Property()
label!: string
@Enum({
items: () => Priority,
customOrder: [Priority.Low, Priority.Medium, Priority.High]
})
priority!: Priority
}
// ...
em.create(Task, { label: 'A', priority: Priority.Low }),
em.create(Task, { label: 'B', priority: Priority.Medium }),
em.create(Task, { label: 'C', priority: Priority.High })
await em.flush();
const tasks = await em.find(Task, {}, { orderBy: { priority: QueryOrder.ASC } });
for (const t of tasks) {
console.log(t.label);
}
// Logs A, B, C
扩展 EntityManager
¥Extending EntityManager
要使用你自己的自定义方法扩展 EntityManager,你可以使用 entityManager
ORM 选项:
¥To extend the EntityManager with your own custom methods, you can use the entityManager
ORM option:
import { MikroORM, EntityManager } from '@mikro-orm/sqlite';
class MyEntityManager extends EntityManager {
myCustomMethod(base: number): number {
return base * Math.random();
}
}
const orm = await MikroORM.init({
entities: [...],
dbName: ':memory:',
entityManager: MyEntityManager,
});
console.log(orm.em instanceof MyEntityManager); // true
const res = orm.em.myCustomMethod(123);