117 lines
6.2 KiB
Plaintext
117 lines
6.2 KiB
Plaintext
++ 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 ecourages) 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:
|
|
|
|
[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:
|
|
|
|
[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.
|
|
|
|
|
|
++ 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. Note that when catching `Exception` you should generally rethrow 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 rethrow, 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 occured you should do that with a new `EntityManager`.
|
|
|
|
|
|
++ 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 dont 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.
|
|
|
|
[php]
|
|
class User
|
|
{
|
|
// ...
|
|
/** @Version @Column(type="integer") */
|
|
private $version;
|
|
// ...
|
|
}
|
|
|
|
Alternatively a datetime type can be used (which maps to an SQL timestamp or datetime):
|
|
|
|
[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.
|
|
|
|
|
|
|
|
|
|
|