transactions-and-concurrency.rst 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. Transactions and Concurrency
  2. ============================
  3. Transaction Demarcation
  4. -----------------------
  5. Transaction demarcation is the task of defining your transaction
  6. boundaries. Proper transaction demarcation is very important
  7. because if not done properly it can negatively affect the
  8. performance of your application. Many databases and database
  9. abstraction layers like PDO by default operate in auto-commit mode,
  10. which means that every single SQL statement is wrapped in a small
  11. transaction. Without any explicit transaction demarcation from your
  12. side, this quickly results in poor performance because transactions
  13. are not cheap.
  14. For the most part, Doctrine 2 already takes care of proper
  15. transaction demarcation for you: All the write operations
  16. (INSERT/UPDATE/DELETE) are queued until ``EntityManager#flush()``
  17. is invoked which wraps all of these changes in a single
  18. transaction.
  19. However, Doctrine 2 also allows (and encourages) you to take over
  20. and control transaction demarcation yourself.
  21. These are two ways to deal with transactions when using the
  22. Doctrine ORM and are now described in more detail.
  23. Approach 1: Implicitly
  24. ~~~~~~~~~~~~~~~~~~~~~~
  25. The first approach is to use the implicit transaction handling
  26. provided by the Doctrine ORM EntityManager. Given the following
  27. code snippet, without any explicit transaction demarcation:
  28. .. code-block:: php
  29. <?php
  30. // $em instanceof EntityManager
  31. $user = new User;
  32. $user->setName('George');
  33. $em->persist($user);
  34. $em->flush();
  35. Since we do not do any custom transaction demarcation in the above
  36. code, ``EntityManager#flush()`` will begin and commit/rollback a
  37. transaction. This behavior is made possible by the aggregation of
  38. the DML operations by the Doctrine ORM and is sufficient if all the
  39. data manipulation that is part of a unit of work happens through
  40. the domain model and thus the ORM.
  41. Approach 2: Explicitly
  42. ~~~~~~~~~~~~~~~~~~~~~~
  43. The explicit alternative is to use the ``Doctrine\DBAL\Connection``
  44. API directly to control the transaction boundaries. The code then
  45. looks like this:
  46. .. code-block:: php
  47. <?php
  48. // $em instanceof EntityManager
  49. $em->getConnection()->beginTransaction(); // suspend auto-commit
  50. try {
  51. //... do some work
  52. $user = new User;
  53. $user->setName('George');
  54. $em->persist($user);
  55. $em->flush();
  56. $em->getConnection()->commit();
  57. } catch (Exception $e) {
  58. $em->getConnection()->rollback();
  59. $em->close();
  60. throw $e;
  61. }
  62. Explicit transaction demarcation is required when you want to
  63. include custom DBAL operations in a unit of work or when you want
  64. to make use of some methods of the ``EntityManager`` API that
  65. require an active transaction. Such methods will throw a
  66. ``TransactionRequiredException`` to inform you of that
  67. requirement.
  68. A more convenient alternative for explicit transaction demarcation
  69. is the use of provided control abstractions in the form of
  70. ``Connection#transactional($func)`` and
  71. ``EntityManager#transactional($func)``. When used, these control
  72. abstractions ensure that you never forget to rollback the
  73. transaction or close the ``EntityManager``, apart from the obvious
  74. code reduction. An example that is functionally equivalent to the
  75. previously shown code looks as follows:
  76. .. code-block:: php
  77. <?php
  78. // $em instanceof EntityManager
  79. $em->transactional(function($em) {
  80. //... do some work
  81. $user = new User;
  82. $user->setName('George');
  83. $em->persist($user);
  84. });
  85. The difference between ``Connection#transactional($func)`` and
  86. ``EntityManager#transactional($func)`` is that the latter
  87. abstraction flushes the ``EntityManager`` prior to transaction
  88. commit and also closes the ``EntityManager`` properly when an
  89. exception occurs (in addition to rolling back the transaction).
  90. Exception Handling
  91. ~~~~~~~~~~~~~~~~~~
  92. When using implicit transaction demarcation and an exception occurs
  93. during ``EntityManager#flush()``, the transaction is automatically
  94. rolled back and the ``EntityManager`` closed.
  95. When using explicit transaction demarcation and an exception
  96. occurs, the transaction should be rolled back immediately and the
  97. ``EntityManager`` closed by invoking ``EntityManager#close()`` and
  98. subsequently discarded, as demonstrated in the example above. This
  99. can be handled elegantly by the control abstractions shown earlier.
  100. Note that when catching ``Exception`` you should generally re-throw
  101. the exception. If you intend to recover from some exceptions, catch
  102. them explicitly in earlier catch blocks (but do not forget to
  103. rollback the transaction and close the ``EntityManager`` there as
  104. well). All other best practices of exception handling apply
  105. similarly (i.e. either log or re-throw, not both, etc.).
  106. As a result of this procedure, all previously managed or removed
  107. instances of the ``EntityManager`` become detached. The state of
  108. the detached objects will be the state at the point at which the
  109. transaction was rolled back. The state of the objects is in no way
  110. rolled back and thus the objects are now out of synch with the
  111. database. The application can continue to use the detached objects,
  112. knowing that their state is potentially no longer accurate.
  113. If you intend to start another unit of work after an exception has
  114. occurred you should do that with a new ``EntityManager``.
  115. Locking Support
  116. ---------------
  117. Doctrine 2 offers support for Pessimistic- and Optimistic-locking
  118. strategies natively. This allows to take very fine-grained control
  119. over what kind of locking is required for your Entities in your
  120. application.
  121. Optimistic Locking
  122. ~~~~~~~~~~~~~~~~~~
  123. Database transactions are fine for concurrency control during a
  124. single request. However, a database transaction should not span
  125. across requests, the so-called "user think time". Therefore a
  126. long-running "business transaction" that spans multiple requests
  127. needs to involve several database transactions. Thus, database
  128. transactions alone can no longer control concurrency during such a
  129. long-running business transaction. Concurrency control becomes the
  130. partial responsibility of the application itself.
  131. Doctrine has integrated support for automatic optimistic locking
  132. via a version field. In this approach any entity that should be
  133. protected against concurrent modifications during long-running
  134. business transactions gets a version field that is either a simple
  135. number (mapping type: integer) or a timestamp (mapping type:
  136. datetime). When changes to such an entity are persisted at the end
  137. of a long-running conversation the version of the entity is
  138. compared to the version in the database and if they don't match, an
  139. ``OptimisticLockException`` is thrown, indicating that the entity
  140. has been modified by someone else already.
  141. You designate a version field in an entity as follows. In this
  142. example we'll use an integer.
  143. .. code-block:: php
  144. <?php
  145. class User
  146. {
  147. // ...
  148. /** @Version @Column(type="integer") */
  149. private $version;
  150. // ...
  151. }
  152. Alternatively a datetime type can be used (which maps to a SQL
  153. timestamp or datetime):
  154. .. code-block:: php
  155. <?php
  156. class User
  157. {
  158. // ...
  159. /** @Version @Column(type="datetime") */
  160. private $version;
  161. // ...
  162. }
  163. Version numbers (not timestamps) should however be preferred as
  164. they can not potentially conflict in a highly concurrent
  165. environment, unlike timestamps where this is a possibility,
  166. depending on the resolution of the timestamp on the particular
  167. database platform.
  168. When a version conflict is encountered during
  169. ``EntityManager#flush()``, an ``OptimisticLockException`` is thrown
  170. and the active transaction rolled back (or marked for rollback).
  171. This exception can be caught and handled. Potential responses to an
  172. OptimisticLockException are to present the conflict to the user or
  173. to refresh or reload objects in a new transaction and then retrying
  174. the transaction.
  175. With PHP promoting a share-nothing architecture, the time between
  176. showing an update form and actually modifying the entity can in the
  177. worst scenario be as long as your applications session timeout. If
  178. changes happen to the entity in that time frame you want to know
  179. directly when retrieving the entity that you will hit an optimistic
  180. locking exception:
  181. You can always verify the version of an entity during a request
  182. either when calling ``EntityManager#find()``:
  183. .. code-block:: php
  184. <?php
  185. use Doctrine\DBAL\LockMode;
  186. use Doctrine\ORM\OptimisticLockException;
  187. $theEntityId = 1;
  188. $expectedVersion = 184;
  189. try {
  190. $entity = $em->find('User', $theEntityId, LockMode::OPTIMISTIC, $expectedVersion);
  191. // do the work
  192. $em->flush();
  193. } catch(OptimisticLockException $e) {
  194. echo "Sorry, but someone else has already changed this entity. Please apply the changes again!";
  195. }
  196. Or you can use ``EntityManager#lock()`` to find out:
  197. .. code-block:: php
  198. <?php
  199. use Doctrine\DBAL\LockMode;
  200. use Doctrine\ORM\OptimisticLockException;
  201. $theEntityId = 1;
  202. $expectedVersion = 184;
  203. $entity = $em->find('User', $theEntityId);
  204. try {
  205. // assert version
  206. $em->lock($entity, LockMode::OPTIMISTIC, $expectedVersion);
  207. } catch(OptimisticLockException $e) {
  208. echo "Sorry, but someone else has already changed this entity. Please apply the changes again!";
  209. }
  210. Important Implementation Notes
  211. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  212. You can easily get the optimistic locking workflow wrong if you
  213. compare the wrong versions. Say you have Alice and Bob editing a
  214. hypothetical blog post:
  215. - Alice reads the headline of the blog post being "Foo", at
  216. optimistic lock version 1 (GET Request)
  217. - Bob reads the headline of the blog post being "Foo", at
  218. optimistic lock version 1 (GET Request)
  219. - Bob updates the headline to "Bar", upgrading the optimistic lock
  220. version to 2 (POST Request of a Form)
  221. - Alice updates the headline to "Baz", ... (POST Request of a
  222. Form)
  223. Now at the last stage of this scenario the blog post has to be read
  224. again from the database before Alice's headline can be applied. At
  225. this point you will want to check if the blog post is still at
  226. version 1 (which it is not in this scenario).
  227. Using optimistic locking correctly, you *have* to add the version
  228. as an additional hidden field (or into the SESSION for more
  229. safety). Otherwise you cannot verify the version is still the one
  230. being originally read from the database when Alice performed her
  231. GET request for the blog post. If this happens you might see lost
  232. updates you wanted to prevent with Optimistic Locking.
  233. See the example code, The form (GET Request):
  234. .. code-block:: php
  235. <?php
  236. $post = $em->find('BlogPost', 123456);
  237. echo '<input type="hidden" name="id" value="' . $post->getId() . '" />';
  238. echo '<input type="hidden" name="version" value="' . $post->getCurrentVersion() . '" />';
  239. And the change headline action (POST Request):
  240. .. code-block:: php
  241. <?php
  242. $postId = (int)$_GET['id'];
  243. $postVersion = (int)$_GET['version'];
  244. $post = $em->find('BlogPost', $postId, \Doctrine\DBAL\LockMode::OPTIMISTIC, $postVersion);
  245. Pessimistic Locking
  246. ~~~~~~~~~~~~~~~~~~~
  247. Doctrine 2 supports Pessimistic Locking at the database level. No
  248. attempt is being made to implement pessimistic locking inside
  249. Doctrine, rather vendor-specific and ANSI-SQL commands are used to
  250. acquire row-level locks. Every Entity can be part of a pessimistic
  251. lock, there is no special metadata required to use this feature.
  252. However for Pessimistic Locking to work you have to disable the
  253. Auto-Commit Mode of your Database and start a transaction around
  254. your pessimistic lock use-case using the "Approach 2: Explicit
  255. Transaction Demarcation" described above. Doctrine 2 will throw an
  256. Exception if you attempt to acquire an pessimistic lock and no
  257. transaction is running.
  258. Doctrine 2 currently supports two pessimistic lock modes:
  259. - Pessimistic Write
  260. (``Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE``), locks the
  261. underlying database rows for concurrent Read and Write Operations.
  262. - Pessimistic Read (``Doctrine\DBAL\LockMode::PESSIMISTIC_READ``),
  263. locks other concurrent requests that attempt to update or lock rows
  264. in write mode.
  265. You can use pessimistic locks in three different scenarios:
  266. 1. Using
  267. ``EntityManager#find($className, $id, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)``
  268. or
  269. ``EntityManager#find($className, $id, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ)``
  270. 2. Using
  271. ``EntityManager#lock($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)``
  272. or
  273. ``EntityManager#lock($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ)``
  274. 3. Using
  275. ``Query#setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)``
  276. or
  277. ``Query#setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_READ)``