diff --git a/docs/feature-compatibility.txt b/docs/feature-compatibility.txt index b4f0406f3..bbb5767e1 100644 --- a/docs/feature-compatibility.txt +++ b/docs/feature-compatibility.txt @@ -136,7 +136,7 @@ The following Eloquent methods are not supported in {+odm-short+}: - *Unsupported* * - Grouping - - Partially supported, use :ref:`Aggregation Builders `. + - Partially supported, use :ref:`Aggregations `. * - Limit and Offset - ✓ diff --git a/docs/includes/fundamentals/transactions/Account.php b/docs/includes/fundamentals/transactions/Account.php new file mode 100644 index 000000000..72b903a50 --- /dev/null +++ b/docs/includes/fundamentals/transactions/Account.php @@ -0,0 +1,13 @@ + 223344, + 'balance' => 5000, + ], + [ + 'number' => 776655, + 'balance' => 100, + ], + ]); + + // begin transaction callback + DB::transaction(function () { + $transferAmount = 200; + + $sender = Account::where('number', 223344)->first(); + $sender->balance -= $transferAmount; + $sender->save(); + + $receiver = Account::where('number', 776655)->first(); + $receiver->balance += $transferAmount; + $receiver->save(); + }); + // end transaction callback + + $sender = Account::where('number', 223344)->first(); + $receiver = Account::where('number', 776655)->first(); + + $this->assertEquals(4800, $sender->balance); + $this->assertEquals(300, $receiver->balance); + } + + public function testTransactionCommit(): void + { + require_once __DIR__ . '/Account.php'; + + Account::truncate(); + + Account::insert([ + [ + 'number' => 223344, + 'balance' => 5000, + ], + [ + 'number' => 776655, + 'balance' => 100, + ], + ]); + + // begin commit transaction + DB::beginTransaction(); + $oldAccount = Account::where('number', 223344)->first(); + + $newAccount = Account::where('number', 776655)->first(); + $newAccount->balance += $oldAccount->balance; + $newAccount->save(); + + $oldAccount->delete(); + DB::commit(); + // end commit transaction + + $acct1 = Account::where('number', 223344)->first(); + $acct2 = Account::where('number', 776655)->first(); + + $this->assertNull($acct1); + $this->assertEquals(5100, $acct2->balance); + } + + public function testTransactionRollback(): void + { + require_once __DIR__ . '/Account.php'; + + Account::truncate(); + Account::insert([ + [ + 'number' => 223344, + 'balance' => 200, + ], + [ + 'number' => 776655, + 'balance' => 0, + ], + [ + 'number' => 990011, + 'balance' => 0, + ], + ]); + + // begin rollback transaction + DB::beginTransaction(); + + $sender = Account::where('number', 223344)->first(); + $receiverA = Account::where('number', 776655)->first(); + $receiverB = Account::where('number', 990011)->first(); + + $amountA = 100; + $amountB = 200; + + $sender->balance -= $amountA; + $receiverA->balance += $amountA; + + $sender->balance -= $amountB; + $receiverB->balance += $amountB; + + if ($sender->balance < 0) { + // insufficient balance, roll back the transaction + DB::rollback(); + } else { + DB::commit(); + } + + // end rollback transaction + + $sender = Account::where('number', 223344)->first(); + $receiverA = Account::where('number', 776655)->first(); + $receiverB = Account::where('number', 990011)->first(); + + $this->assertEquals(200, $sender->balance); + $this->assertEquals(0, $receiverA->balance); + $this->assertEquals(0, $receiverB->balance); + } +} diff --git a/docs/query-builder.txt b/docs/query-builder.txt index 9650df09b..5249e2911 100644 --- a/docs/query-builder.txt +++ b/docs/query-builder.txt @@ -878,7 +878,7 @@ specified query: :end-before: end query elemMatch To learn more about regular expression queries in MongoDB, see -the :manual:`$elemMatch operator ` +the :manual:`$elemMatch operator ` in the {+server-docs-name+}. .. _laravel-query-builder-cursor-timeout: diff --git a/docs/transactions.txt b/docs/transactions.txt index ee70f8c8b..3cb3c2c5b 100644 --- a/docs/transactions.txt +++ b/docs/transactions.txt @@ -9,71 +9,150 @@ Transactions :values: tutorial .. meta:: - :keywords: php framework, odm, code example + :keywords: php framework, odm, rollback, commit, callback, code example, acid, atomic, consistent, isolated, durable -MongoDB transactions require the following software and topology: +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + +Overview +-------- + +In this guide, you can learn how to perform a **transaction** in MongoDB by +using the {+odm-long+}. Transactions let you run a sequence of write operations +that update the data only after the transaction is committed. + +If the transaction fails, the PHP library that manages MongoDB operations +for {+odm-short+} ensures that MongoDB discards all the changes made within +the transaction before they become visible. This property of transactions +that ensures that all changes within a transaction are either applied or +discarded is called **atomicity**. + +MongoDB performs write operations on single documents atomically. If you +need atomicity in write operations on multiple documents or data consistency +across multiple documents for your operations, run them in a multi-document +transaction. + +Multi-document transactions are **ACID compliant** because MongoDB +guarantees that the data in your transaction operations remains consistent, +even if the driver encounters unexpected errors. + +Learn how to perform transactions in the following sections of this guide: + +- :ref:`laravel-transaction-requirements` +- :ref:`laravel-transaction-callback` +- :ref:`laravel-transaction-commit` +- :ref:`laravel-transaction-rollback` + +.. tip:: + + To learn more about transactions in MongoDB, see :manual:`Transactions ` + in the {+server-docs-name+}. + +.. _laravel-transaction-requirements: + +Requirements and Limitations +---------------------------- + +To perform transactions in MongoDB, you must use the following MongoDB +version and topology: - MongoDB version 4.0 or later - A replica set deployment or sharded cluster -You can find more information :manual:`in the MongoDB docs ` +MongoDB server and {+odm-short+} have the following limitations: -.. code-block:: php +- In MongoDB versions 4.2 and earlier, write operations performed within a + transaction must be on existing collections. In MongoDB versions 4.4 and + later, the server automatically creates collections as necessary when + you perform write operations in a transaction. To learn more about this + limitation, see :manual:`Create Collections and Indexes in a Transaction ` + in the {+server-docs-name+}. +- MongoDB does not support nested transactions. If you attempt to start a + transaction within another one, the extension raises a ``RuntimeException``. + To learn more about this limitation, see :manual:`Transactions and Sessions ` + in the {+server-docs-name+}. +- The {+odm-long+} does not support the database testing traits + ``Illuminate\Foundation\Testing\DatabaseTransactions`` and ``Illuminate\Foundation\Testing\RefreshDatabase``. + As a workaround, you can create migrations with the ``Illuminate\Foundation\Testing\DatabaseMigrations`` + trait to reset the database after each test. - DB::transaction(function () { - User::create(['name' => 'john', 'age' => 19, 'title' => 'admin', 'email' => 'john@example.com']); - DB::collection('users')->where('name', 'john')->update(['age' => 20]); - DB::collection('users')->where('name', 'john')->delete(); - }); +.. _laravel-transaction-callback: -.. code-block:: php +Run a Transaction in a Callback +------------------------------- + +This section shows how you can run a transaction in a callback. - // begin a transaction - DB::beginTransaction(); - User::create(['name' => 'john', 'age' => 19, 'title' => 'admin', 'email' => 'john@example.com']); - DB::collection('users')->where('name', 'john')->update(['age' => 20]); - DB::collection('users')->where('name', 'john')->delete(); +When using this method of running a transaction, all the code in the +callback method runs as a single transaction. - // commit changes - DB::commit(); +In the following example, the transaction consists of write operations that +transfer the funds from a bank account, represented by the ``Account`` model, +to another account: -To abort a transaction, call the ``rollBack`` method at any point during the transaction: +.. literalinclude:: /includes/fundamentals/transactions/TransactionsTest.php + :language: php + :dedent: + :start-after: begin transaction callback + :end-before: end transaction callback + +You can optionally pass the maximum number of times to retry a failed transaction as the second parameter as shown in the following code example: .. code-block:: php + :emphasize-lines: 4 - DB::beginTransaction(); - User::create(['name' => 'john', 'age' => 19, 'title' => 'admin', 'email' => 'john@example.com']); + DB::transaction(function() { + // transaction code + }, + retries: 5, + ); - // Abort the transaction, discarding any data created as part of it - DB::rollBack(); +.. _laravel-transaction-commit: +Begin and Commit a Transaction +------------------------------ -.. note:: +This section shows how to start and commit a transaction. - Transactions in MongoDB cannot be nested. DB::beginTransaction() function - will start new transactions in a new created or existing session and will - raise the RuntimeException when transactions already exist. See more in - MongoDB official docs :manual:`Transactions and Sessions `. +To use this method of starting and committing a transaction, call the +``DB::beginTransaction()`` method to start the transaction. Then, call the +``DB::commit()`` method to end the transaction, which applies all the updates +performed within the transaction. -.. code-block:: php +In the following example, the balance from the first account is moved to the +second account, after which the first account is deleted: - DB::beginTransaction(); - User::create(['name' => 'john', 'age' => 20, 'title' => 'admin']); +.. literalinclude:: /includes/fundamentals/transactions/TransactionsTest.php + :language: php + :dedent: + :emphasize-lines: 1,9 + :start-after: begin commit transaction + :end-before: end commit transaction - // This call to start a nested transaction will raise a RuntimeException - DB::beginTransaction(); - DB::collection('users')->where('name', 'john')->update(['age' => 20]); - DB::commit(); - DB::rollBack(); +.. _laravel-transaction-rollback: -Database Testing ----------------- +Roll Back a Transaction +----------------------- -For testing, the traits ``Illuminate\Foundation\Testing\DatabaseTransactions`` -and ``Illuminate\Foundation\Testing\RefreshDatabase`` are not yet supported. -Instead, create migrations and use the ``DatabaseMigrations`` trait to reset -the database after each test: +This section shows how to roll back a transaction. A rollback reverts all the +write operations performed within that transaction. This means that the +data is reverted to its state before the transaction. -.. code-block:: php +To perform the rollback, call the ``DB::rollback()`` function anytime before +the transaction is committed. + +In the following example, the transaction consists of write operations that +transfer funds from one account, represented by the ``Account`` model, to +multiple other accounts. If the sender account has insufficient funds, the +transaction is rolled back, and none of the models are updated: + +.. literalinclude:: /includes/fundamentals/transactions/TransactionsTest.php + :language: php + :dedent: + :emphasize-lines: 1,18,20 + :start-after: begin rollback transaction + :end-before: end rollback transaction - use Illuminate\Foundation\Testing\DatabaseMigrations;