10. Doctrine Internals explained¶
Object relational mapping is a complex topic and sufficiently understanding how Doctrine works internally helps you use its full power.
10.1. How Doctrine keeps track of Objects¶
Doctrine uses the Identity Map pattern to track objects. Whenever you fetch an object from the database, Doctrine will keep a reference to this object inside its UnitOfWork. The array holding all the entity references is two-levels deep and has the keys “root entity name” and “id”. Since Doctrine allows composite keys the id is a sorted, serialized version of all the key columns.
This allows Doctrine room for optimizations. If you call the EntityManager and ask for an entity with a specific ID twice, it will return the same instance:
public function testIdentityMap(): void
{
$objectA = $this->entityManager->find('EntityName', 1);
$objectB = $this->entityManager->find('EntityName', 1);
$this->assertSame($objectA, $objectB)
}
Only one SELECT query will be fired against the database here. In the second
EntityManager#find()
call Doctrine will check the identity map first and
doesn’t need to make that database roundtrip.
Even if you get a proxy object first then fetch the object by the same id you will still end up with the same reference:
public function testIdentityMapReference(): void
{
$objectA = $this->entityManager->getReference('EntityName', 1);
// check for proxyinterface
$this->assertInstanceOf('Doctrine\Persistence\Proxy', $objectA);
$objectB = $this->entityManager->find('EntityName', 1);
$this->assertSame($objectA, $objectB)
}
The identity map being indexed by primary keys only allows shortcuts when you
ask for objects by primary key. Assume you have the following persons
table:
id | name
-------------
1 | Benjamin
2 | Bud
Take the following example where two consecutive calls are made against a repository to fetch an entity by a set of criteria:
public function testIdentityMapRepositoryFindBy()
{
$repository = $this->entityManager->getRepository('Person');
$objectA = $repository->findOneBy(array('name' => 'Benjamin'));
$objectB = $repository->findOneBy(array('name' => 'Benjamin'));
$this->assertSame($objectA, $objectB);
}
This query will still return the same references and $objectA and $objectB are indeed referencing the same object. However when checking your SQL logs you will realize that two queries have been executed against the database. Doctrine 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 Person object Doctrine first gets the primary key from the row and check if it already has an object inside the UnitOfWork with that primary key. In our example it finds an object and decides to return this instead of creating a new one.
The identity map has a second use-case. When you call EntityManager#flush
Doctrine will ask the identity map for all objects that are currently managed.
This means you don’t have to call EntityManager#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.
The following code WILL update your database with the changes made to the
Person
object, even if you did not call EntityManager#persist
:
<?php
$user = $entityManager->find("Person", 1);
$user->setName("Guilherme");
$entityManager->flush();
10.2. How Doctrine Detects Changes¶
Doctrine is a data-mapper that tries to achieve persistence-ignorance (PI). This means you map php objects into a relational database that don’t necessarily know about the database at all. A natural question would now be, “how does Doctrine even detect objects have changed?”.
For this Doctrine keeps a second map inside the UnitOfWork. Whenever you fetch an object from the database Doctrine will keep a copy of all the properties and associations inside the UnitOfWork. Because variables in the PHP language are subject to “copy-on-write” the memory usage of a PHP request that only reads objects from the database is the same as if Doctrine did not keep this variable copy. Only if you start changing variables PHP will create new variables internally that consume new memory.
Now whenever you call EntityManager#flush
Doctrine will iterate over the
Identity Map and for each object compares 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 SQL UPDATE operation. Only the fields
that actually changed are updated.
This process has an obvious performance impact. The larger the size of the UnitOfWork is, the longer this computation takes. There are several ways to optimize the performance of the Flush Operation:
Mark entities as read only. These entities can only be inserted or removed, but are never updated. They are omitted in the changeset calculation.
エンティティを読み取り専用としてマークします。これらのエンティティは挿入または削除のみ可能で、更新されることはありません。それらは変更セットの計算では省略されます。Temporarily mark entities as read only. If you have a very large UnitOfWork but know that a large set of entities has not changed, just mark them as read only with
$entityManager->getUnitOfWork()->markReadOnly($entity)
.エンティティを一時的に読み取り専用としてマークします。 UnitOfWork が非常に大きいが、多数のエンティティが変更されていないことがわかっている場合は、$entityManager->getUnitOfWork()->markReadOnly($entity) を使用してそれらを読み取り専用としてマークするだけです。Flush only a single entity with
$entityManager->flush($entity)
.$entityManager->flush($entity) を使用して単一のエンティティのみをフラッシュします。Use Change Tracking Policies to use more explicit strategies of notifying the UnitOfWork what objects/properties changed.
変更追跡ポリシーを使用して、どのオブジェクト/プロパティが変更されたかを UnitOfWork に通知する、より明示的な戦略を使用します。
Note
Flush only a single entity with $entityManager->flush($entity)
is deprecated and will be removed in ORM 3.0.
(Details)
10.3. Query Internals¶
10.4. The different ORM Layers¶
Doctrine ships with a set of layers with different responsibilities. This section gives a short explanation of each layer.
10.4.1. Hydration¶
Responsible for creating a final result from a raw database statement and a result-set mapping object. The developer can choose which kind of result they wish to be hydrated. Default result-types include:
SQL to Entities
SQL からエンティティへSQL to structured Arrays
SQL から構造化配列へSQL to simple scalar result arrays
SQL から単純なスカラー結果配列へSQL to a single result variable
単一の結果変数への SQL
Hydration to entities and arrays is one of the most complex parts of Doctrine algorithm-wise. It can build results with for example:
Single table selects
単一のテーブル選択Joins with n:1 or 1:n cardinality, grouping belonging to the same parent.
n:1 または 1:n カーディナリティで結合し、同じ親に属するグループ化。Mixed results of objects and scalar values
オブジェクトとスカラー値の混合結果Hydration of results by a given scalar value as key.
キーとして指定されたスカラー値による結果のハイドレーション。
10.4.2. Persisters¶
tbr
10.4.3. UnitOfWork¶
tbr
10.4.4. ResultSetMapping¶
tbr
10.4.5. DQL Parser¶
tbr
10.4.6. SQLWalker¶
tbr
10.4.7. EntityManager¶
tbr
10.4.8. ClassMetadataFactory¶
tbr