Skip to content

Merge 4.2 into 4.3 #2855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/feature-compatibility.txt
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ The following Eloquent methods are not supported in {+odm-short+}:
- *Unsupported*

* - Grouping
- Partially supported, use :ref:`Aggregation Builders <laravel-query-builder-aggregates>`.
- Partially supported, use :ref:`Aggregations <laravel-query-builder-aggregations>`.

* - Limit and Offset
- ✓
Expand Down
13 changes: 13 additions & 0 deletions docs/includes/fundamentals/transactions/Account.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace App\Models;

use MongoDB\Laravel\Eloquent\Model;

class Account extends Model
{
protected $connection = 'mongodb';
protected $fillable = ['number', 'balance'];
}
144 changes: 144 additions & 0 deletions docs/includes/fundamentals/transactions/TransactionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Models\Account;
use Illuminate\Support\Facades\DB;
use MongoDB\Laravel\Tests\TestCase;

class TransactionsTest extends TestCase
{
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testTransactionCallback(): void
{
require_once __DIR__ . '/Account.php';

Account::truncate();

Account::insert([
[
'number' => 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);
}
}
2 changes: 1 addition & 1 deletion docs/query-builder.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <reference/operator/query/elemMatch/>`
the :manual:`$elemMatch operator </reference/operator/query/elemMatch/>`
in the {+server-docs-name+}.

.. _laravel-query-builder-cursor-timeout:
Expand Down
165 changes: 122 additions & 43 deletions docs/transactions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 </core/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 </core/transactions/>`
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 </core/transactions/#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 </core/transactions/#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' => '[email protected]']);
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' => '[email protected]']);
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' => '[email protected]']);
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 </core/transactions/#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;