12. Transactions and Concurrency

12.1. Transaction Demarcation

Transaction demarcation is the task of defining your transaction boundaries. Proper transaction demarcation is very important because if not done properly it can negatively affect the performance of your application. Many databases and database abstraction layers like PDO by default operate in auto-commit mode, which means that every single SQL statement is wrapped in a small transaction. Without any explicit transaction demarcation from your side, this quickly results in poor performance because transactions are not cheap.

トランザクション境界は、トランザクション境界を定義するタスクです。適切なトランザクション境界設定は非常に重要です。適切に行わないと、アプリケーションのパフォーマンスに悪影響を及ぼす可能性があるためです。多くのデータベースと PDO などのデータベース抽象化レイヤーは、デフォルトで自動コミット モードで動作します。つまり、すべての SQL ステートメントが小さなトランザクションにラップされます。明示的なトランザクション境界がないと、トランザクションは安くないため、すぐにパフォーマンスが低下します。

For the most part, Doctrine ORM already takes care of proper transaction demarcation for you: All the write operations (INSERT/UPDATE/DELETE) are queued until EntityManager#flush() is invoked which wraps all of these changes in a single transaction.

ほとんどの場合、Doctrine ORM はすでに適切なトランザクション境界を処理しています。すべての書き込み操作 (INSERT/UPDATE/DELETE) は、単一のトランザクションでこれらすべての変更をラップする EntityManager#flush() が呼び出されるまでキューに入れられます。

However, Doctrine ORM also allows (and encourages) you to take over and control transaction demarcation yourself.

ただし、Doctrine ORM では、トランザクション境界を自分で引き継いで制御することも許可 (および推奨) されます。

These are two ways to deal with transactions when using the Doctrine ORM and are now described in more detail.

これらは、Doctrine ORM を使用するときにトランザクションを処理する 2 つの方法であり、より詳細に説明されています。

12.1.1. Approach 1: Implicitly

The first approach is to use the implicit transaction handling provided by the Doctrine ORM EntityManager. Given the following code snippet, without any explicit transaction demarcation:

最初のアプローチは、Doctrine ORM EntityManager によって提供される暗黙的なトランザクション処理を使用することです。次のコード スニペットを考えると、明示的なトランザクションの境界はありません。

<?php
// $em instanceof EntityManager
$user = new User;
$user->setName('George');
$em->persist($user);
$em->flush();

Since we do not do any custom transaction demarcation in the above code, EntityManager#flush() will begin and commit/rollback a transaction. This behavior is made possible by the aggregation of the DML operations by the Doctrine ORM and is sufficient if all the data manipulation that is part of a unit of work happens through the domain model and thus the ORM.

上記のコードではカスタム トランザクションの境界設定を行っていないため、EntityManager#flush() が開始され、トランザクションがコミット/ロールバックされます。この動作は、Doctrine ORM による DML 操作の集約によって可能になり、作業単位の一部であるすべてのデータ操作がドメイン モデル、したがって ORM を通じて行われる場合に十分です。

12.1.2. Approach 2: Explicitly

The explicit alternative is to use the Doctrine\DBAL\Connection API directly to control the transaction boundaries. The code then looks like this:

明示的な代替手段は、Doctrine\DBAL\ConnectionAPI を直接使用してトランザクション境界を制御することです。コードは次のようになります。

<?php
// $em instanceof EntityManager
$em->getConnection()->beginTransaction(); // suspend auto-commit
try {
    // ... do some work
    $user = new User;
    $user->setName('George');
    $em->persist($user);
    $em->flush();
    $em->getConnection()->commit();
} catch (Exception $e) {
    $em->getConnection()->rollBack();
    throw $e;
}

Explicit transaction demarcation is required when you want to include custom DBAL operations in a unit of work or when you want to make use of some methods of the EntityManager API that require an active transaction. Such methods will throw a TransactionRequiredException to inform you of that requirement.

作業単位にカスタム DBAL 操作を含める場合、またはアクティブなトランザクションを必要とする EntityManager API のいくつかのメソッドを使用する場合は、明示的なトランザクション境界が必要です。このようなメソッドは、TransactionRequiredException をスローして、その要件を通知します。

A more convenient alternative for explicit transaction demarcation is the use of provided control abstractions in the form of Connection#transactional($func) and EntityManager#transactional($func). When used, these control abstractions ensure that you never forget to rollback the transaction, in addition to the obvious code reduction. An example that is functionally equivalent to the previously shown code looks as follows:

明示的なトランザクション境界のより便利な代替手段は、Connection#transactional($func) および EntityManager#transactional($func) の形式で提供されているコントロールの抽象化を使用することです。これらのコントロールの抽象化を使用すると、トランザクションのロールバックを忘れることがなくなります。明らかなコード削減に。前に示したコードと機能的に同等の例は、次のようになります。

<?php
// $em instanceof EntityManager
$em->transactional(function($em) {
    // ... do some work
    $user = new User;
    $user->setName('George');
    $em->persist($user);
});

Warning

警告

For historical reasons, EntityManager#transactional($func) will return true whenever the return value of $func is loosely false. Some examples of this include array(), "0", "", 0, and null.

歴史的な理由から、EntityManager#transactional($func) は、$func の戻り値がおおよそ false である場合はいつでも true を返します。この例には、array()、"0"、""、0、および null が含まれます。

The difference between Connection#transactional($func) and EntityManager#transactional($func) is that the latter abstraction flushes the EntityManager prior to transaction commit and rolls back the transaction when an exception occurs.

Connection#transactional($func) と EntityManager#transactional($func) の違いは、後者の抽象化では、トランザクションコミットの前に EntityManager をフラッシュし、例外が発生したときにトランザクションをロールバックすることです。

12.1.3. Exception Handling

When using implicit transaction demarcation and an exception occurs during EntityManager#flush(), the transaction is automatically rolled back and the EntityManager closed.

暗黙的なトランザクション境界を使用し、EntityManager#flush() 中に例外が発生すると、トランザクションは自動的にロールバックされ、EntityManager が閉じられます。

When using explicit transaction demarcation and an exception occurs, the transaction should be rolled back immediately and the EntityManager closed by invoking EntityManager#close() and subsequently discarded, as demonstrated in the example above. This can be handled elegantly by the control abstractions shown earlier. Note that when catching Exception you should generally re-throw the exception. If you intend to recover from some exceptions, catch them explicitly in earlier catch blocks (but do not forget to rollback the transaction and close the EntityManager there as well). All other best practices of exception handling apply similarly (i.e. either log or re-throw, not both, etc.).

明示的なトランザクション境界を使用し、例外が発生した場合、トランザクションはすぐにロールバックされ、EntityManager#close() を呼び出すことによって EntityManager が閉じられ、その後、上記の例で示されているように破棄されます。これは、前に示したコントロールの抽象化によってエレガントに処理できます。例外をキャッチするときは、通常、例外を再スローする必要があることに注意してください。一部の例外から回復する場合は、以前の catch ブロックでそれらを明示的にキャッチします (ただし、トランザクションをロールバックし、そこで EntityManager も閉じることを忘れないでください)。例外処理の他のすべてのベスト プラクティスも同様に適用されます (つまり、両方ではなく、ログまたは再スローのいずれかなど)。

As a result of this procedure, all previously managed or removed instances of the EntityManager become detached. The state of the detached objects will be the state at the point at which the transaction was rolled back. The state of the objects is in no way rolled back and thus the objects are now out of synch with the database. The application can continue to use the detached objects, knowing that their state is potentially no longer accurate.

この手順の結果、EntityManager の以前に管理または削除されたすべてのインスタンスが切り離されます。切り離されたオブジェクトの状態は、トランザクションがロールバックされた時点の状態になります。オブジェクトの状態は決してロールバックされないため、オブジェクトはデータベースと同期していません。アプリケーションは、分離されたオブジェクトの状態が正確でなくなる可能性があることを認識して、それらのオブジェクトを引き続き使用できます。

If you intend to start another unit of work after an exception has occurred you should do that with a new EntityManager.

例外が発生した後に別の作業単位を開始する場合は、新しい EntityManager を使用して開始する必要があります。

12.2. Locking Support

Doctrine ORM offers support for Pessimistic- and Optimistic-locking strategies natively. This allows to take very fine-grained control over what kind of locking is required for your Entities in your application.

Doctrine ORM は、悲観的および楽観的ロック戦略をネイティブにサポートします。これにより、アプリケーション内のエンティティに必要なロックの種類を非常にきめ細かく制御できます。

12.2.1. Optimistic Locking

Database transactions are fine for concurrency control during a single request. However, a database transaction should not span across requests, the so-called “user think time”. Therefore a long-running “business transaction” that spans multiple requests needs to involve several database transactions. Thus, database transactions alone can no longer control concurrency during such a long-running business transaction. Concurrency control becomes the partial responsibility of the application itself.

データベース トランザクションは、単一の要求中の同時実行制御に適しています。ただし、データベース トランザクションは、いわゆる「ユーザーの思考時間」と呼ばれる複数の要求にまたがってはなりません。したがって、複数のリクエストにまたがる同時進行の「ビジネス トランザクション」には、複数のデータベース トランザクションが必要です。したがって、databasetransactions だけでは、このような並行ビジネス トランザクション中の同時実行性を制御することはできなくなります。同時実行制御は、アプリケーション自体の部分的な責任になります。

Doctrine has integrated support for automatic optimistic locking via a version field. In this approach any entity that should be protected against concurrent modifications during long-running business transactions gets a version field that is either a simple number (mapping type: integer) or a timestamp (mapping type: datetime). When changes to such an entity are persisted at the end of a long-running conversation the version of the entity is compared to the version in the database and if they don’t match, an OptimisticLockException is thrown, indicating that the entity has been modified by someone else already.

Doctrine には、バージョン フィールドによる自動楽観的ロックのサポートが統合されています。このアプローチでは、長時間実行されるビジネス トランザクション中に同時変更から保護する必要があるエンティティは、単純な数値 (マッピング タイプ: 整数) またはタイムスタンプ (マッピング タイプ: 日時) のいずれかであるバージョン フィールドを取得します。このようなエンティティへの変更が長時間の会話の最後に永続化されると、エンティティのバージョンがデータベース内のバージョンと比較され、それらが一致しない場合、エンティティが他の誰かによって既に変更されていることを示す OptimisticLockException がスローされます。 .

You designate a version field in an entity as follows. In this example we’ll use an integer.

次のように、エンティティのバージョン フィールドを指定します。この例では、整数を使用します。

Alternatively a datetime type can be used (which maps to a SQL timestamp or datetime):

または、datetime 型を使用することもできます (SQLtimestamp または datetime にマップされます)。

Version numbers (not timestamps) should however be preferred as they can not potentially conflict in a highly concurrent environment, unlike timestamps where this is a possibility, depending on the resolution of the timestamp on the particular database platform.

ただし、バージョン番号 (タイムスタンプではない) を優先する必要があります。これは、特定のデータベース プラットフォームでのタイムスタンプの解決に応じて、競合が発生する可能性があるタイムスタンプとは異なり、高度な並行環境で競合する可能性がないためです。

When a version conflict is encountered during EntityManager#flush(), an OptimisticLockException is thrown and the active transaction rolled back (or marked for rollback). This exception can be caught and handled. Potential responses to an OptimisticLockException are to present the conflict to the user or to refresh or reload objects in a new transaction and then retrying the transaction.

EntityManager#flush() 中にバージョンの競合が発生すると、OptimisticLockException がスローされ、アクティブなトランザクションがロールバックされます (またはロールバックのマークが付けられます)。この例外はキャッチして処理できます。 OptimisticLockException に対する潜在的な応答は、競合をユーザーに提示するか、新しいトランザクションでオブジェクトを更新または再ロードしてから、トランザクションを再試行することです。

With PHP promoting a share-nothing architecture, the time between showing an update form and actually modifying the entity can in the worst scenario be as long as your applications session timeout. If changes happen to the entity in that time frame you want to know directly when retrieving the entity that you will hit an optimistic locking exception:

何も共有しないアーキテクチャを促進する PHP では、更新フォームを表示してからエンティティを実際に変更するまでの時間が、最悪の場合、アプリケーションのセッション タイムアウトと同じくらい長くなる可能性があります。その時間枠内にエンティティに変更が発生した場合、エンティティを取得するときに直接知りたい場合は、楽観的ロック例外が発生します。

You can always verify the version of an entity during a request either when calling EntityManager#find():

エンティティのバージョンは、EntityManager#find() を呼び出すときにいつでも確認できます。

<?php
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\OptimisticLockException;

$theEntityId = 1;
$expectedVersion = 184;

try {
    $entity = $em->find('User', $theEntityId, LockMode::OPTIMISTIC, $expectedVersion);

    // do the work

    $em->flush();
} catch(OptimisticLockException $e) {
    echo "Sorry, but someone else has already changed this entity. Please apply the changes again!";
}

Or you can use EntityManager#lock() to find out:

または、EntityManager#lock() を使用して確認できます。

<?php
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\OptimisticLockException;

$theEntityId = 1;
$expectedVersion = 184;

$entity = $em->find('User', $theEntityId);

try {
    // assert version
    $em->lock($entity, LockMode::OPTIMISTIC, $expectedVersion);

} catch(OptimisticLockException $e) {
    echo "Sorry, but someone else has already changed this entity. Please apply the changes again!";
}

12.2.1.1. Important Implementation Notes

You can easily get the optimistic locking workflow wrong if you compare the wrong versions. Say you have Alice and Bob editing a hypothetical blog post:

間違ったバージョンを比較すると、楽観的ロックのワークフローは簡単に間違ってしまう可能性があります。 Alice と Bob が架空のブログ投稿を編集しているとします。

  • Alice reads the headline of the blog post being “Foo”, at optimistic lock version 1 (GET Request)

    アリスは、楽観的ロック バージョン 1 (GET 要求) である「Foo」というブログ記事の見出しを読みます。

  • Bob reads the headline of the blog post being “Foo”, at optimistic lock version 1 (GET Request)

    ボブは、ブログ記事の見出しが「Foo」である、楽観的ロック バージョン 1 (GET 要求) を読みます。

  • Bob updates the headline to “Bar”, upgrading the optimistic lock version to 2 (POST Request of a Form)

    Bob は見出しを「バー」に更新し、楽観的ロックバージョンを 2 にアップグレードします (フォームの POST リクエスト)

  • Alice updates the headline to “Baz”, … (POST Request of a Form)

    Alice は見出しを「Baz」に更新します… (aForm の POST リクエスト)

Now at the last stage of this scenario the blog post has to be read again from the database before Alice’s headline can be applied. At this point you will want to check if the blog post is still at version 1 (which it is not in this scenario).

このシナリオの最後の段階で、Alice の見出しを適用する前に、ブログ投稿をデータベースから再度読み取る必要があります。この時点で、ブログ投稿がまだバージョン 1 であるかどうかを確認する必要があります (このシナリオにはありません)。

Using optimistic locking correctly, you have to add the version as an additional hidden field (or into the SESSION for more safety). Otherwise you cannot verify the version is still the one being originally read from the database when Alice performed her GET request for the blog post. If this happens you might see lost updates you wanted to prevent with Optimistic Locking.

楽観的ロックを正しく使用するには、追加の非表示フィールドとしてバージョンを追加する必要があります (または、安全性を高めるために SESSION に追加する必要があります)。そうしないと、Alice がブログ投稿に対して彼女の GET 要求を実行したときに、バージョンがデータベースから最初に読み取られたものであることを確認できません。これが発生した場合、Optimistic Locking で防止したかった更新が失われる可能性があります。

See the example code, The form (GET Request):

コード例、フォーム (GET リクエスト) を参照してください。

<?php
$post = $em->find('BlogPost', 123456);

echo '<input type="hidden" name="id" value="' . $post->getId() . '" />';
echo '<input type="hidden" name="version" value="' . $post->getCurrentVersion() . '" />';

And the change headline action (POST Request):

そして、見出しの変更アクション (POST リクエスト):

<?php
$postId = (int)$_GET['id'];
$postVersion = (int)$_GET['version'];

$post = $em->find('BlogPost', $postId, \Doctrine\DBAL\LockMode::OPTIMISTIC, $postVersion);

12.2.2. Pessimistic Locking

Doctrine ORM supports Pessimistic Locking at the database level. No attempt is being made to implement pessimistic locking inside Doctrine, rather vendor-specific and ANSI-SQL commands are used to acquire row-level locks. Every Entity can be part of a pessimistic lock, there is no special metadata required to use this feature.

Doctrine ORM は、データベースレベルでペシミスティックロックをサポートしています。 Doctrine 内で悲観的ロックを実装する試みは行われておらず、行レベルのロックを取得するためにベンダー固有の ANSI-SQL コマンドが使用されています。すべてのエンティティは悲観的ロックの一部になることができます。この機能を使用するために特別なメタデータは必要ありません。

However for Pessimistic Locking to work you have to disable the Auto-Commit Mode of your Database and start a transaction around your pessimistic lock use-case using the “Approach 2: Explicit Transaction Demarcation” described above. Doctrine ORM will throw an Exception if you attempt to acquire an pessimistic lock and no transaction is running.

ただし、ペシミスティック ロックを機能させるには、データベースの自動コミット モードを無効にし、前述の「アプローチ 2: 明示的なトランザクション境界」を使用して、ペシミスティック ロックのユースケースの周りでトランザクションを開始する必要があります。 Doctrine ORM は、悲観的ロックを取得しようとしてトランザクションが実行されていない場合、例外をスローします。

Doctrine ORM currently supports two pessimistic lock modes:

Doctrine ORM は現在、2 つの悲観的ロックモードをサポートしています:

  • Pessimistic Write (Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE), locks the underlying database rows for concurrent Read and Write Operations.

    Pessimistic Write(Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE) は、同時読み取りおよび書き込み操作のために、基礎となるデータベース行をロックします。

  • Pessimistic Read (Doctrine\DBAL\LockMode::PESSIMISTIC_READ), locks other concurrent requests that attempt to update or lock rows in write mode.

    ペシミスティック読み取り (Doctrine\DBAL\LockMode::PESSIMISTIC_READ) は、書き込みモードで行を更新またはロックしようとする他の同時リクエストをロックします。

You can use pessimistic locks in four different scenarios:

ペシミスティック ロックは、次の 4 つの異なるシナリオで使用できます。

  1. Using EntityManager#find($className, $id, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE) or EntityManager#find($className, $id, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ)

    UsingEntityManager#find($className, $id, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)orEntityManager#find($className, $id, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ)

  2. Using EntityManager#lock($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE) or EntityManager#lock($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ)

    UsingEntityManager#lock($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)orEntityManager#lock($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ)

  3. Using EntityManager#refresh($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE) or EntityManager#refresh($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ)

    UsingEntityManager#refresh($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)orEntityManager#refresh($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ)

  4. Using Query#setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE) or Query#setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_READ)

    UsingQuery#setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)orQuery#setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_READ)

Table Of Contents

Previous topic

11. Association Updates: Owning Side and Inverse Side

11. 関連付けの更新: 所有側と逆側

Next topic

13. Batch Processing

13. バッチ処理

This Page

Fork me on GitHub