transactions.rst 9.4 KB
Newer Older
1 2 3 4 5 6
Transactions
============

A ``Doctrine\DBAL\Connection`` provides a PDO-like API for
transaction management, with the methods
``Connection#beginTransaction()``, ``Connection#commit()`` and
7
``Connection#rollBack()``.
8 9 10 11 12 13 14 15 16 17

Transaction demarcation with the Doctrine DBAL looks as follows:

::

    <?php
    $conn->beginTransaction();
    try{
        // do stuff
        $conn->commit();
18
    } catch (\Exception $e) {
19
        $conn->rollBack();
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
        throw $e;
    }

Alternatively, the control abstraction
``Connection#transactional($func)`` can be used to make the code
more concise and to make sure you never forget to rollback the
transaction in the case of an exception. The following code snippet
is functionally equivalent to the previous one:

::

    <?php
    $conn->transactional(function($conn) {
        // do stuff
    });

The ``Doctrine\DBAL\Connection`` also has methods to control the
transaction isolation level as supported by the underlying
database. ``Connection#setTransactionIsolation($level)`` and
39
``Connection#getTransactionIsolation()`` can be used for that purpose.
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
The possible isolation levels are represented by the following
constants:

::

    <?php
    Connection::TRANSACTION_READ_UNCOMMITTED
    Connection::TRANSACTION_READ_COMMITTED
    Connection::TRANSACTION_REPEATABLE_READ
    Connection::TRANSACTION_SERIALIZABLE

The default transaction isolation level of a
``Doctrine\DBAL\Connection`` is chosen by the underlying platform
but it is always at least READ\_COMMITTED.

Transaction Nesting
-------------------

A ``Doctrine\DBAL\Connection`` also adds support for nesting
transactions, or rather propagating transaction control up the call
stack. For that purpose, the ``Connection`` class keeps an internal
counter that represents the nesting level and is
increased/decreased as ``beginTransaction()``, ``commit()`` and
63
``rollBack()`` are invoked. ``beginTransaction()`` increases the
64
nesting level whilst
65
``commit()`` and ``rollBack()`` decrease the nesting level. The nesting level starts at 0. Whenever the nesting level transitions from 0 to 1, ``beginTransaction()`` is invoked on the underlying driver connection and whenever the nesting level transitions from 1 to 0, ``commit()`` or ``rollBack()`` is invoked on the underlying driver, depending on whether the transition was caused by ``Connection#commit()`` or ``Connection#rollBack()``.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82

What this means is that transaction control is basically passed to
code higher up in the call stack and the inner transaction block is
ignored, with one important exception that is described further
below. Do not confuse this with "real" nested transactions or
savepoints. These are not supported by Doctrine. There is always
only a single, real database transaction.

To visualize what this means in practice, consider the following
example:

::

    <?php
    // $conn instanceof Doctrine\DBAL\Connection
    $conn->beginTransaction(); // 0 => 1, "real" transaction started
    try {
83

84
        ...
85

86 87 88 89 90
        // nested transaction block, this might be in some other API/library code that is
        // unaware of the outer transaction.
        $conn->beginTransaction(); // 1 => 2
        try {
            ...
91

92
            $conn->commit(); // 2 => 1
93
        } catch (\Exception $e) {
94
            $conn->rollBack(); // 2 => 1, transaction marked for rollback only
95 96
            throw $e;
        }
97

98
        ...
99

100
        $conn->commit(); // 1 => 0, "real" transaction committed
101
    } catch (\Exception $e) {
102
        $conn->rollBack(); // 1 => 0, "real" transaction rollback
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
        throw $e;
    }

However,
**a rollback in a nested transaction block will always mark the current transaction so that the only possible outcome of the transaction is to be rolled back**.
That means in the above example, the rollback in the inner
transaction block marks the whole transaction for rollback only.
Even if the nested transaction block would not rethrow the
exception, the transaction is marked for rollback only and the
commit of the outer transaction would trigger an exception, leading
to the final rollback. This also means that you can not
successfully commit some changes in an outer transaction if an
inner transaction block fails and issues a rollback, even if this
would be the desired behavior (i.e. because the nested operation is
"optional" for the purpose of the outer transaction block). To
achieve that, you need to restructure your application logic so as
to avoid nesting transaction blocks. If this is not possible
because the nested transaction blocks are in a third-party API
you're out of luck.

xelan's avatar
xelan committed
123
All that is guaranteed to the inner transaction is that it still
124 125 126
happens atomically, all or nothing, the transaction just gets a
wider scope and the control is handed to the outer scope.

127 128 129
.. note::

    The transaction nesting described here is a debated
130
    feature that has its critics. Form your own opinion. We recommend
131 132 133 134 135
    avoiding nesting transaction blocks when possible, and most of the
    time, it is possible. Transaction control should mostly be left to
    a service layer and not be handled in data access objects or
    similar.

136
.. warning::
137

138
    Directly invoking ``PDO#beginTransaction()``,
139
    ``PDO#commit()`` or ``PDO#rollBack()`` or the corresponding methods
140
    on the particular ``Doctrine\DBAL\Driver\Connection`` instance in
141
    use bypasses the transparent transaction nesting that is provided
142 143 144 145
    by ``Doctrine\DBAL\Connection`` and can therefore corrupt the
    nesting level, causing errors with broken transaction boundaries
    that may be hard to debug.

146 147 148 149 150 151 152 153 154
Auto-commit mode
----------------

A ``Doctrine\DBAL\Connection`` supports setting the auto-commit mode
to control whether queries should be automatically wrapped into a
transaction or directly be committed to the database.
By default a connection runs in auto-commit mode which means
that it is non-transactional unless you start a transaction explicitly
via ``beginTransaction()``. To have a connection automatically open up
155
a new transaction on ``connect()`` and after ``commit()`` or ``rollBack()``,
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
you can disable auto-commit mode with ``setAutoCommit(false)``.

::

    <?php
    // define connection parameters $params and initialize driver $driver

    $conn = new \Doctrine\DBAL\Connection($params, $driver);

    $conn->setAutoCommit(false); // disables auto-commit
    $conn->connect(); // connects and immediately starts a new transaction

    try {
        // do stuff
        $conn->commit(); // commits transaction and immediately starts a new one
    } catch (\Exception $e) {
172
        $conn->rollBack(); // rolls back transaction and immediately starts a new one
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
    }

    // still transactional

.. note::

    Changing auto-commit mode during an active transaction, implicitly
    commits active transactions for that particular connection.

::

    <?php
    // define connection parameters $params and initialize driver $driver

    $conn = new \Doctrine\DBAL\Connection($params, $driver);

    // we are in auto-commit mode
    $conn->beginTransaction();

    // disable auto-commit, commits currently active transaction
    $conn->setAutoCommit(false); // also causes a new transaction to be started

    // no-op as auto-commit is already disabled
    $conn->setAutoCommit(false);

    // enable auto-commit again, commits currently active transaction
    $conn->setAutoCommit(true); // does not start a new transaction automatically

Committing or rolling back an active transaction will of course only
open up a new transaction automatically if the particular action causes
the transaction context of a connection to terminate.
That means committing or rolling back nested transactions are not affected
by this behaviour.

::

    <?php
    // we are not in auto-commit mode, transaction is active

    try {
        // do stuff

        $conn->beginTransaction(); // start inner transaction, nesting level 2

        try {
            // do stuff
            $conn->commit(); // commits inner transaction, does not start a new one
        } catch (\Exception $e) {
221
            $conn->rollBack(); // rolls back inner transaction, does not start a new one
222 223 224 225 226 227
        }

        // do stuff

        $conn->commit(); // commits outer transaction, and immediately starts a new one
    } catch (\Exception $e) {
228
        $conn->rollBack(); // rolls back outer transaction, and immediately starts a new one
229 230 231
    }

To initialize a ``Doctrine\DBAL\Connection`` with auto-commit disabled,
Andrew M's avatar
Andrew M committed
232
you can also use the ``Doctrine\DBAL\Configuration`` container to modify the
233 234
default auto-commit mode via ``Doctrine\DBAL\Configuration::setAutoCommit(false)``
and pass it to a ``Doctrine\DBAL\Connection`` when instantiating.
235 236 237 238 239 240

Error handling
--------------

In order to handle errors related to deadlocks or lock wait timeouts,
you can use Doctrine built-in transaction exceptions.
241
All transaction exceptions where retrying makes sense have a marker interface: ``Doctrine\DBAL\Exception\RetryableException``.
242 243 244 245 246 247 248 249
A practical example is as follows:

::

    <?php

    try {
        // process stuff
250
    } catch (\Doctrine\DBAL\Exception\RetryableException $e) {
251 252 253 254 255
        // retry the processing
    }

If you need stricter control, you can catch the concrete exceptions directly:

256
- ``Doctrine\DBAL\Exception\DeadlockException``: this can happen when each member
257
  of a group of actions is waiting for some other member to release a shared lock.
258
- ``Doctrine\DBAL\Exception\LockWaitTimeoutException``: this exception happens when
259
  a transaction has to wait a considerable amount of time to obtain a lock, even if
260
  a deadlock is not involved.
261