Skip to content

HTTPS clone URL

Subversion checkout URL

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