diff --git a/docs/en/cookbook/validation-of-documents.rst b/docs/en/cookbook/validation-of-documents.rst index cc25ed603a..72f89736f2 100644 --- a/docs/en/cookbook/validation-of-documents.rst +++ b/docs/en/cookbook/validation-of-documents.rst @@ -86,7 +86,7 @@ Now validation is performed whenever you call ``DocumentManager#persist($order)`` or when you call ``DocumentManager#flush()`` and an order is about to be updated. Any Exception that happens in the lifecycle callbacks will be cached by -the DocumentManager and the current transaction is rolled back. +the DocumentManager. Of course you can do any type of primitive checks, not null, email-validation, string size, integer and date ranges in your diff --git a/docs/en/reference/architecture.rst b/docs/en/reference/architecture.rst index dc9df0f140..9ff13338ce 100644 --- a/docs/en/reference/architecture.rst +++ b/docs/en/reference/architecture.rst @@ -56,7 +56,7 @@ A document instance can be characterized as being NEW, MANAGED, DETACHED or REMO DocumentManager and a UnitOfWork. - A REMOVED document instance is an instance with a persistent identity, associated with a DocumentManager, that will be removed - from the database upon transaction commit. + from the database upon UnitOfWork commit. Persistent fields ~~~~~~~~~~~~~~~~~ @@ -103,7 +103,7 @@ persistent objects. Transactional write-behind ~~~~~~~~~~~~~~~~~~~~~~~~~~ -An ``DocumentManager`` and the underlying ``UnitOfWork`` employ a +The ``DocumentManager`` and the underlying ``UnitOfWork`` employ a strategy called "transactional write-behind" that delays the execution of query statements in order to execute them in the most efficient way and to execute them at the end of a transaction so diff --git a/docs/en/reference/events.rst b/docs/en/reference/events.rst index 1e12a2fe0a..679e1481e2 100644 --- a/docs/en/reference/events.rst +++ b/docs/en/reference/events.rst @@ -42,7 +42,7 @@ Now we can add some event listeners to the ``$evm``. Let's create a $evm->addEventListener([self::preFoo, self::postFoo], $this); } - public function preFoo(EventArgs $e): void + public function preFoo(EventArgs $e): void { $this->preFooInvoked = true; } @@ -345,6 +345,38 @@ follow this restrictions very carefully since operations in the wrong event may produce lots of different errors, such as inconsistent data and lost updates/persists/removes. +Handling Transactional Flushes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a flush operation is executed in a transaction, all queries inside a lifecycle event listener also have to make use +of the session used during the flush operation. This session object is exposed through the ``LifecycleEventArgs`` +parameter passed to the listener. Passing the session to queries ensures that the query will become part of the +transaction and will see data that has not been committed yet. + +.. code-block:: php + + isInTransaction()) { + // Do something + } + + // Pass the session to any query you execute + $eventArgs->getDocumentManager()->createQueryBuilder(User::class) + // Query logic + ->getQuery(['session' => $eventArgs->session]) + ->execute(); + } + +.. note:: + + Event listeners are only called during the first transaction attempt. If the transaction is retried, event listeners + will not be invoked again. Make sure to run any persistence logic through the UnitOfWork instead of modifying data + directly through queries run in an event listener. + prePersist ~~~~~~~~~~ @@ -693,8 +725,8 @@ Define the ``EventTest`` class with a ``postCollectionLoad()`` method: } } -Load ClassMetadata Event ------------------------- +loadClassMetadata +~~~~~~~~~~~~~~~~~ When the mapping information for a document is read, it is populated in to a ``ClassMetadata`` instance. You can hook in to diff --git a/docs/en/reference/transactions-and-concurrency.rst b/docs/en/reference/transactions-and-concurrency.rst index 17fc0a5af4..dee35e9571 100644 --- a/docs/en/reference/transactions-and-concurrency.rst +++ b/docs/en/reference/transactions-and-concurrency.rst @@ -9,26 +9,78 @@ Transactions As per the `documentation `_, MongoDB write operations are "atomic on the level of a single document". -Even when updating multiple documents within a single write operation, -though the modification of each document is atomic, -the operation as a whole is not and other operations may interleave. +Even when updating multiple documents within a single write operation, though the modification of each document is +atomic, the operation as a whole is not and other operations may interleave. -As stated in the `FAQ `_, -"MongoDB does not support multi-document transactions" and neither does Doctrine MongoDB ODM. +Transaction support +~~~~~~~~~~~~~~~~~~~ + +MongoDB supports multi-document transactions on replica sets (starting in MongoDB 4.2) and sharded clusters (MongoDB +4.4). Standalone topologies do not support multi-document transactions. + +Transaction Support in Doctrine MongoDB ODM +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + Transaction support in MongoDB ODM was introduced in version 2.7. + +You can instruct the ODM to use transactions when writing changes to the databases by enabling the +``useTransactionalFlush`` setting in your configuration: + +.. code-block:: php + + $config = new Configuration(); + $config->setUseTransactionalFlush(true); + // Other configuration + + $dm = DocumentManager::create(null, $config); + +From then onwards, any call to ``DocumentManager::flush`` will start a transaction, apply the write operations, then +commit the transaction. + +To enable or disable transaction usage for a single flush operation, use the ``withTransaction`` write option when +calling ``DocumentManager::flush``: + +.. code-block:: php + + // To explicitly enable transaction for this write + $dm->flush(['withTransaction' => true]); + + // To disable transaction usage for a write, regardless of the ``useTransactionalFlush`` config: + $dm->flush(['withTransaction' => false]); + +.. note:: + + Please note that transactions are only used for write operations executed during the ``flush`` operation. For any + other operations, e.g. manually executed queries or aggregation pipelines, transactions will not be used and you + will have to rely on the MongoDB driver's transaction mechanism. + +Lifecycle Events and Transactions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using transactional flushes, either through the configuration or explicitly, there are a couple of important things +to note regarding lifecycle events. Due to the way MongoDB transactions work, it is possible that ODM attempts write +operations multiple times. However, to preserve the expectation that lifecycle events are only triggered once per flush +operation, lifecycle events will not be dispatched when the transaction is retried. This maintains current functionality +when a lifecycle event modifies the unit of work, as this change is automatically carried over when the transaction is +retried. -Limitation -~~~~~~~~~~ -At the moment, Doctrine MongoDB ODM does not provide any native strategy to emulate multi-document transactions. +Lifecycle events now expose a ``MongoDB\Driver\Session`` object which needs to be used if it is set. Since MongoDB +transactions are not tied to the connection but only to a session, any command that should be part of the transaction +needs to be told about the session to be used. This does not only apply to write commands, but also to read commands +that need to see the transaction state. If a session is given in a lifecycle event, this session should always be used +regardless of whether a transaction is active or not. -Workaround -~~~~~~~~~~ -To work around this limitation, one can utilize `two phase commits `_. -Concurrency ------------ +Other Concurrency Controls +-------------------------- -Doctrine MongoDB ODM offers native support for pessimistic and optimistic locking strategies. -This allows for very fine-grained control over what kind of locking is required for documents in your application. +Multi-Document transactions provide certain guarantees regarding your database writes and prevent two simultaneous write +operations from interfering with each other. Depending on your use case, this is not enough, as the transactional +guarantee will only apply once you start writing to the database as part of the ``DocumentManager::flush()`` call. This +could still lead to data loss if you replace data that was written to the database by a different process in between you +reading the data and starting the transaction. To solve this problem, optimistic and pessimistic locking strategies can +be used, allowing for fine-grained control over what kind of locking is required for documents in your application. .. _transactions_and_concurrency_optimistic_locking: