工作单元和事务
MikroORM 使用身份映射模式来跟踪对象。每当你从数据库中获取对象时,MikroORM 都会在其 UnitOfWork
中保留对该对象的引用。
¥MikroORM uses the Identity Map pattern to track objects. Whenever you fetch an object from the database, MikroORM will keep a reference to this object inside its UnitOfWork
.
这为 MikroORM 提供了优化空间。如果你两次调用 EntityManager 并请求具有特定 ID 的实体,它将返回相同的实例:
¥This allows MikroORM room for optimizations. If you call the EntityManager and ask for an entity with a specific ID twice, it will return the same instance:
const jon1 = await em.findOne(Author, 1);
const jon2 = await em.findOne(Author, 1);
// identity map in action
console.log(jon1 === jon2); // true
这里只会对数据库触发一个 SELECT 查询。在第二个 findOne()
调用中,MikroORM 将首先检查身份映射,并跳过数据库往返,因为它会发现实体已经加载。
¥Only one SELECT query will be fired against the database here. In the second findOne()
call MikroORM will check the identity map first and will skip the database round trip as it will find the entity already loaded.
通过主键索引的身份映射仅当你通过主键请求对象时才允许快捷方式。当你通过其他属性查询时,你仍将获得相同的引用,但将进行两个单独的数据库调用:
¥The identity map being indexed by primary keys only allows shortcuts when you ask for objects by primary key. When you query by other properties, you will still get the same reference, but two separate database calls will be made:
const jon1 = await em.findOne(Author, { name: 'Jon Snow' });
const jon2 = await em.findOne(Author, { name: 'Jon Snow' });
// identity map in action
console.log(jon1 === jon2); // true
MikroORM 仅通过 ID 识别对象,因此对不同条件的查询必须转到数据库,即使该查询之前刚刚执行过。但是,MikroORM 不会创建第二个 Author
对象,而是首先从行中获取主键,并检查它是否已经在 UnitOfWork
中有一个具有该主键的对象。
¥MikroORM only knows objects by id, so a query for different criteria has to go to the database, even if it was executed just before. But instead of creating a second Author
object MikroORM first gets the primary key from the row and checks if it already has an object inside the UnitOfWork
with that primary key.
持久化托管实体
¥Persisting Managed Entities
身份映射有第二个用例。当你调用 em.flush()
时,MikroORM 将向身份映射询问当前管理的所有对象。这意味着你不必一遍又一遍地调用 em.persist()
将已知对象传递给 EntityManager
。对于已知实体,这是一个无操作,但会导致编写大量代码,让其他开发者感到困惑。
¥The identity map has a second use-case. When you call em.flush()
, MikroORM will ask the identity map for all objects that are currently managed. This means you don't have to call em.persist()
over and over again to pass known objects to the EntityManager
. This is a NO-OP for known entities, but leads to much code written that is confusing to other developers.
即使你没有调用 em.persist()
,以下代码也会使用对 Author
对象所做的更改更新你的数据库:
¥The following code WILL update your database with the changes made to the Author
object, even if you did not call em.persist()
:
const jon = await em.findOne(Author, 1);
jon.email = 'foo@bar.com';
await em.flush();
具有显式主键的实体
¥Entities with explicit primary key
当你 em.persist()
具有主键值的新实体时,它将自动添加到身份映射中。这意味着基于其主键对 em.findOne()
的后续调用将仅返回相同的非托管实体实例,而不是查询数据库。
¥When you em.persist()
a new entity which has the primary key value, it will be automatically added to the identity map. This means that a following call to em.findOne()
based on its primary key will just return the same unmanaged entity instance instead of querying the database.
此类实体已添加到身份映射中,但仍处于未管理状态 - 它还没有对
EntityManager
的引用。¥Such entity is added to the identity map, but still remains unmanaged - it does not have a reference to the
EntityManager
yet.
// primary key value provided, will be added to the identity map
const jon = em.create(Author, {
id: 1,
name: 'Jon',
email: 'foo@bar.com',
});
// this will not query the database
const jon2 = await em.findOne(Author, 1);
console.log(jon === jon2); // true
await em.flush(); // this inserts the entity
如果你将 em.persist()
调用为没有主键值的实体,em.findOne()
调用也会检测到它并自动刷新以首先获取值。
¥If you called em.persist()
an entity without the primary key value, the em.findOne()
call would detect it as well and flush automatically to get the value first.
// primary key value not provided
const jon = em.create(Author, {
name: 'Jon',
email: 'foo@bar.com',
});
// this will trigger auto flush and insert the entity, then query for it
const jon2 = await em.findOne(Author, 1);
console.log(jon === jon2); // true
await em.flush(); // this is a no-op
MikroORM 如何检测更改
¥How MikroORM Detects Changes
MikroORM 是一个试图实现持久性无知 (PI) 的数据映射器。这意味着你将 JS 对象映射到关系数据库中,而该数据库根本不需要了解数据库。现在一个自然的问题是,"MikroORM 如何检测对象是否已更改?"。
¥MikroORM is a data-mapper that tries to achieve persistence-ignorance (PI). This means you map JS objects into a relational database that do not necessarily know about the database at all. A natural question would now be, "how does MikroORM even detect objects have changed?".
为此,MikroORM 在 UnitOfWork
中保留了第二个映射。每当你从数据库中获取对象时,MikroORM 都会保留 UnitOfWork
内所有属性和关联的副本。
¥For this MikroORM keeps a second map inside the UnitOfWork
. Whenever you fetch an object from the database MikroORM will keep a copy of all the properties and associations inside the UnitOfWork
.
现在,每当你调用 em.flush()
时,MikroORM 都会遍历你之前标记为通过 em.persist()
持久化的所有实体。对于每个对象,它将比较原始属性和关联值与当前在对象上设置的值。如果检测到更改,则将对象排队等待 UPDATE 操作。只更新实际更改的字段。
¥Now whenever you call em.flush()
MikroORM will iterate over all entities you previously marked for persisting via em.persist()
. For each object it will compare the original property and association values with the values that are currently set on the object. If changes are detected then the object is queued for a UPDATE operation. Only the fields that actually changed are updated.
隐式事务
¥Implicit Transactions
拥有工作单元的首要含义是它允许自动处理事务。
¥First and most important implication of having Unit of Work is that it allows handling transactions automatically.
当你调用 em.flush()
时,所有计算的更改都会在数据库事务中查询(如果给定的驱动程序支持)。这意味着你可以通过调用 em.persist()
来控制事务的边界,一旦所有更改都准备好了,调用 flush()
将在事务内运行它们。
¥When you call em.flush()
, all computed changes are queried inside a database transaction (if supported by given driver). This means that you can control the boundaries of transactions simply by calling em.persist()
and once all your changes are ready, simply calling flush()
will run them inside a transaction.
你还可以通过
em.transactional(cb)
手动控制事务边界。¥You can also control the transaction boundaries manually via
em.transactional(cb)
.
const user = await em.findOne(User, 1);
user.email = 'foo@bar.com';
const car = new Car();
user.cars.add(car);
// thanks to bi-directional cascading we only need to persist user entity
// flushing will create a transaction, insert new car and update user with new email
await em.persist(user).flush();
你可以在 事务和并发 页面中找到有关事务的更多信息。
¥You can find more information about transactions in Transactions and concurrency page.
刷新模式
¥Flush Modes
刷新策略由当前运行的 EntityManager
的 flushMode
给出。
¥The flushing strategy is given by the flushMode
of the current running EntityManager
.
-
FlushMode.COMMIT
-EntityManager
延迟刷新,直到当前事务提交。¥
FlushMode.COMMIT
- TheEntityManager
delays the flush until the current Transaction is committed. -
FlushMode.AUTO
- 这是默认模式,它仅在必要时刷新EntityManager
。¥
FlushMode.AUTO
- This is the default mode, and it flushes theEntityManager
only if necessary. -
FlushMode.ALWAYS
- 在每次查询之前刷新EntityManager
。¥
FlushMode.ALWAYS
- Flushes theEntityManager
before every query.
FlushMode.AUTO
将尝试检测我们正在查询的实体上的更改,如果有重叠则刷新:
¥FlushMode.AUTO
will try to detect changes on the entity we are querying, and flush if there is an overlap:
// querying for author will trigger auto-flush if we have new author persisted
const a1 = new Author(...);
em.persist(a1);
const r1 = await em.find(Author, {});
// querying author won't trigger auto-flush if we have new book, but no changes on author
const b4 = new Book(...);
em.persist(b4);
const r2 = await em.find(Author, {});
// but querying for book will trigger auto-flush
const r3 = await em.find(Book, {});
还可以检测到对托管实体的更改,尽管这仅基于简单的脏检查,没有查询分析。
¥Changes on managed entities are also detected, although this works only based on simple dirty checks, no query analyses in place.
const book = await em.findOne(Book, 1);
book.price = 1000;
// triggers auto-flush because of the changed `price`
const r1 = await em.find(Book, { price: { $gt: 500 } });
// triggers auto-flush too, the book entity is dirty
const r2 = await em.find(Book, { name: /foo.*/ });
我们可以在不同的地方设置刷新模式:
¥We can set the flush mode on different places:
-
在 ORM 配置中通过
Options.flushMode
¥in the ORM config via
Options.flushMode
-
对于通过
em.setFlushMode()
给定的EntityManager
实例(及其分叉)¥for given
EntityManager
instance (and its forks) viaem.setFlushMode()
-
对于通过
em.fork({ flushMode })
给定的EntityManager
分叉¥for given
EntityManager
fork viaem.fork({ flushMode })
-
对于通过
qb.setFlushMode()
给定的 QueryBuilder 实例¥for given QueryBuilder instance via
qb.setFlushMode()
-
对于通过
em.transactional(..., { flushMode })
给定的事务范围¥for given transaction scope via
em.transactional(..., { flushMode })
更改跟踪和性能注意事项
¥Change tracking and performance considerations
当我们使用默认的 FlushMode.AUTO
时,我们需要检测对托管实体所做的更改。为此,每个属性都被动态地重新定义为 get/set
对。虽然这对终端用户来说应该是透明的,但如果我们需要非常频繁地读取某些属性(例如数百万次),则可能导致性能问题。
¥When we use the default FlushMode.AUTO
, we need to detect changes done on managed entities. To do this, every property is dynamically redefined as a get/set
pair. While this should be all transparent to end users, it can lead to performance issues if we need to read some properties very often (e.g. millions of times).
标量主键从未定义为
get/set
对。¥Scalar primary keys are never defined as
get/set
pairs.
为了缓解这种情况,我们可以在属性级别禁用更改跟踪。更改此类属性将不再触发自动刷新机制,但在显式 flush()
调用期间将受到尊重。
¥To mitigate this, we can disable change tracking on a property level. Changing such properties will no longer trigger the auto flush mechanism, but they will be respected during explicit flush()
call.
@Property({ trackChanges: false })
code!: string;
本文档的这一部分受到 doctrine 内部文档 的极大启发,因为这里的行为几乎相同。
¥This part of documentation is highly inspired by doctrine internals docs as the behaviour here is pretty much the same.