123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- Transactions and Concurrency
- ============================
- 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.
- For the most part, Doctrine 2 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.
- However, Doctrine 2 also allows (and encourages) you to take over
- and control transaction demarcation yourself.
- These are two ways to deal with transactions when using the
- Doctrine ORM and are now described in more detail.
- 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:
- .. code-block:: php
- <?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.
- 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:
- .. code-block:: php
- <?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();
- $em->close();
- 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.
- 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 or close the ``EntityManager``, apart from the obvious
- code reduction. An example that is functionally equivalent to the
- previously shown code looks as follows:
- .. code-block:: php
- <?php
- // $em instanceof EntityManager
- $em->transactional(function($em) {
- //... do some work
- $user = new User;
- $user->setName('George');
- $em->persist($user);
- });
- The difference between ``Connection#transactional($func)`` and
- ``EntityManager#transactional($func)`` is that the latter
- abstraction flushes the ``EntityManager`` prior to transaction
- commit and also closes the ``EntityManager`` properly when an
- exception occurs (in addition to rolling back the transaction).
- Exception Handling
- ~~~~~~~~~~~~~~~~~~
- When using implicit transaction demarcation and an exception occurs
- during ``EntityManager#flush()``, the transaction is automatically
- rolled back and the ``EntityManager`` closed.
- 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.).
- 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.
- If you intend to start another unit of work after an exception has
- occurred you should do that with a new ``EntityManager``.
- Locking Support
- ---------------
- Doctrine 2 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.
- 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.
- 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.
- You designate a version field in an entity as follows. In this
- example we'll use an integer.
- .. code-block:: php
- <?php
- class User
- {
- // ...
- /** @Version @Column(type="integer") */
- private $version;
- // ...
- }
- Alternatively a datetime type can be used (which maps to a SQL
- timestamp or datetime):
- .. code-block:: php
- <?php
- class User
- {
- // ...
- /** @Version @Column(type="datetime") */
- private $version;
- // ...
- }
- 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.
- 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:
- You can always verify the version of an entity during a request
- either when calling ``EntityManager#find()``:
- .. code-block:: php
- <?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:
- .. code-block:: php
- <?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!";
- }
- 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 reads the headline of the blog post being "Foo", at
- optimistic lock version 1 (GET Request)
- - Bob reads the headline of the blog post being "Foo", at
- optimistic lock version 1 (GET Request)
- - Bob updates the headline to "Bar", upgrading the optimistic lock
- version to 2 (POST Request of a Form)
- - Alice updates the headline to "Baz", ... (POST Request of a
- Form)
- 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).
- 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.
- See the example code, The form (GET Request):
- .. code-block:: php
- <?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):
- .. code-block:: php
- <?php
- $postId = (int)$_GET['id'];
- $postVersion = (int)$_GET['version'];
-
- $post = $em->find('BlogPost', $postId, \Doctrine\DBAL\LockMode::OPTIMISTIC, $postVersion);
- Pessimistic Locking
- ~~~~~~~~~~~~~~~~~~~
- Doctrine 2 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.
- 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 2 will throw an
- Exception if you attempt to acquire an pessimistic lock and no
- transaction is running.
- Doctrine 2 currently supports two pessimistic lock modes:
- - Pessimistic Write
- (``Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE``), locks the
- underlying database rows for concurrent Read and Write Operations.
- - Pessimistic Read (``Doctrine\DBAL\LockMode::PESSIMISTIC_READ``),
- locks other concurrent requests that attempt to update or lock rows
- in write mode.
- You can use pessimistic locks in three different scenarios:
- 1. Using
- ``EntityManager#find($className, $id, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)``
- or
- ``EntityManager#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)``
- 3. Using
- ``Query#setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)``
- or
- ``Query#setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_READ)``
|