Skip to content

Commit 2d36b93

Browse files
alcaeusklinsonlevon80999
committed
Add support for transactions
Co-authored-by: klinson <klinson@163.com> Co-authored-by: levon80999 <levonb@ucraft.com>
1 parent 46aa0fc commit 2d36b93

File tree

6 files changed

+619
-10
lines changed

6 files changed

+619
-10
lines changed

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ This package adds functionalities to the Eloquent model and Query builder for Mo
3737
- [Query Builder](#query-builder)
3838
- [Basic Usage](#basic-usage-2)
3939
- [Available operations](#available-operations)
40+
- [Transactions](#transactions)
4041
- [Schema](#schema)
4142
- [Basic Usage](#basic-usage-3)
4243
- [Geospatial indexes](#geospatial-indexes)
@@ -968,6 +969,46 @@ If you are familiar with [Eloquent Queries](http://laravel.com/docs/queries), th
968969
### Available operations
969970
To see the available operations, check the [Eloquent](#eloquent) section.
970971

972+
Transactions
973+
-------
974+
Transactions require MongoDB version ^4.0 as well as deployment of replica set or sharded clusters. You can find more information [in the MongoDB docs](https://docs.mongodb.com/manual/core/transactions/)
975+
976+
### Basic Usage
977+
978+
Transaction supports all operations.
979+
980+
```php
981+
DB::transaction(function () {
982+
User::create(['name' => 'john', 'age' => 19, 'title' => 'admin', 'email' => 'klinsonup@gmail.com']);
983+
DB::collection('users')->where('name', 'john')->update(['age' => 20]);
984+
DB::collection('users')->where('name', 'john')->delete();
985+
});
986+
```
987+
988+
```php
989+
// begin a transaction
990+
DB::beginTransaction();
991+
User::create(['name' => 'john', 'age' => 19, 'title' => 'admin', 'email' => 'klinsonup@gmail.com']);
992+
DB::collection('users')->where('name', 'john')->update(['age' => 20]);
993+
DB::collection('users')->where('name', 'john')->delete();
994+
995+
// you can commit your changes
996+
DB::commit();
997+
998+
// you can also rollback them
999+
//DB::rollBack();
1000+
```
1001+
**NOTE:** 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 [Transactions and Sessions](https://www.mongodb.com/docs/manual/core/transactions/#transactions-and-sessions)
1002+
```php
1003+
// This code will raise a RuntimeException
1004+
DB::beginTransaction();
1005+
User::create(['name' => 'john', 'age' => 20, 'title' => 'admin']);
1006+
DB::beginTransaction()
1007+
DB::collection('users')->where('name', 'john')->update(['age' => 20]);
1008+
DB::commit()
1009+
DB::rollBack();
1010+
```
1011+
9711012
Schema
9721013
------
9731014
The database driver also has (limited) schema builder support. You can easily manipulate collections and set indexes.

phpunit.xml.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
<file>tests/QueryBuilderTest.php</file>
2020
<file>tests/QueryTest.php</file>
2121
</testsuite>
22+
<testsuite name="transaction">
23+
<file>tests/TransactionTest.php</file>
24+
</testsuite>
2225
<testsuite name="model">
2326
<file>tests/ModelTest.php</file>
2427
<file>tests/RelationsTest.php</file>

src/Concerns/ManagesTransactions.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
namespace Jenssegers\Mongodb\Concerns;
4+
5+
use Closure;
6+
use MongoDB\Client;
7+
use MongoDB\Driver\Exception\RuntimeException;
8+
use MongoDB\Driver\Session;
9+
use function MongoDB\with_transaction;
10+
11+
trait ManagesTransactions
12+
{
13+
protected ?Session $session = null;
14+
15+
/**
16+
* @return Client
17+
*/
18+
abstract public function getMongoClient();
19+
20+
public function getSession(): ?Session
21+
{
22+
return $this->session;
23+
}
24+
25+
private function getSessionOrCreate(): Session
26+
{
27+
if ($this->session === null) {
28+
$this->session = $this->getMongoClient()->startSession();
29+
}
30+
31+
return $this->session;
32+
}
33+
34+
private function getSessionOrThrow(): Session
35+
{
36+
$session = $this->getSession();
37+
38+
if ($session === null) {
39+
throw new RuntimeException('There is no active session.');
40+
}
41+
42+
return $session;
43+
}
44+
45+
/**
46+
* Use the existing or create new session and start a transaction in session.
47+
*
48+
* In version 4.0, MongoDB supports multi-document transactions on replica sets.
49+
* In version 4.2, MongoDB introduces distributed transactions, which adds support for multi-document transactions on sharded clusters and incorporates the existing support for multi-document transactions on replica sets.
50+
*
51+
* @see https://docs.mongodb.com/manual/core/transactions/
52+
*/
53+
public function beginTransaction(array $options = []): void
54+
{
55+
$this->getSessionOrCreate()->startTransaction($options);
56+
$this->transactions = 1;
57+
}
58+
59+
/**
60+
* Commit transaction in this session
61+
*/
62+
public function commit(): void
63+
{
64+
$this->getSessionOrThrow()->commitTransaction();
65+
$this->transactions = 0;
66+
}
67+
68+
/**
69+
* Rollback transaction in this session
70+
*/
71+
public function rollBack($toLevel = null): void
72+
{
73+
$this->getSessionOrThrow()->abortTransaction();
74+
$this->transactions = 0;
75+
}
76+
77+
/**
78+
* Static transaction function realize the with_transaction functionality provided by MongoDB.
79+
*
80+
* @param int $attempts
81+
*/
82+
public function transaction(Closure $callback, $attempts = 1, array $options = []): mixed
83+
{
84+
$attemptsLeft = $attempts;
85+
$callbackResult = null;
86+
87+
$session = $this->getSessionOrCreate();
88+
89+
$callbackFunction = function (Session $session) use ($callback, &$attemptsLeft, &$callbackResult) {
90+
$attemptsLeft--;
91+
92+
if ($attemptsLeft < 0) {
93+
$session->abortTransaction();
94+
95+
return;
96+
}
97+
98+
$callbackResult = $callback();
99+
};
100+
101+
with_transaction($session, $callbackFunction, $options);
102+
103+
return $callbackResult;
104+
}
105+
}

src/Connection.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
use Illuminate\Database\Connection as BaseConnection;
66
use Illuminate\Support\Arr;
77
use InvalidArgumentException;
8+
use Jenssegers\Mongodb\Concerns\ManagesTransactions;
89
use MongoDB\Client;
910

1011
class Connection extends BaseConnection
1112
{
13+
use ManagesTransactions;
14+
1215
/**
1316
* The MongoDB database handler.
1417
*

src/Query/Builder.php

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,8 @@ public function getFresh($columns = [], $returnLazy = false)
345345
$options = array_merge($options, $this->options);
346346
}
347347

348+
$options = $this->inheritConnectionOptions($options);
349+
348350
// Execute aggregation
349351
$results = iterator_to_array($this->collection->aggregate($pipeline, $options));
350352

@@ -355,12 +357,10 @@ public function getFresh($columns = [], $returnLazy = false)
355357
// Return distinct results directly
356358
$column = isset($this->columns[0]) ? $this->columns[0] : '_id';
357359

360+
$options = $this->inheritConnectionOptions();
361+
358362
// Execute distinct
359-
if ($wheres) {
360-
$result = $this->collection->distinct($column, $wheres);
361-
} else {
362-
$result = $this->collection->distinct($column);
363-
}
363+
$result = $this->collection->distinct($column, $wheres ?: [], $options);
364364

365365
return new Collection($result);
366366
} // Normal query
@@ -406,6 +406,8 @@ public function getFresh($columns = [], $returnLazy = false)
406406
$options = array_merge($options, $this->options);
407407
}
408408

409+
$options = $this->inheritConnectionOptions($options);
410+
409411
// Execute query and get MongoCursor
410412
$cursor = $this->collection->find($wheres, $options);
411413

@@ -580,8 +582,9 @@ public function insert(array $values)
580582
$values = [$values];
581583
}
582584

583-
// Batch insert
584-
$result = $this->collection->insertMany($values);
585+
$options = $this->inheritConnectionOptions();
586+
587+
$result = $this->collection->insertMany($values, $options);
585588

586589
return 1 == (int) $result->isAcknowledged();
587590
}
@@ -591,7 +594,9 @@ public function insert(array $values)
591594
*/
592595
public function insertGetId(array $values, $sequence = null)
593596
{
594-
$result = $this->collection->insertOne($values);
597+
$options = $this->inheritConnectionOptions();
598+
599+
$result = $this->collection->insertOne($values, $options);
595600

596601
if (1 == (int) $result->isAcknowledged()) {
597602
if ($sequence === null) {
@@ -613,6 +618,8 @@ public function update(array $values, array $options = [])
613618
$values = ['$set' => $values];
614619
}
615620

621+
$options = $this->inheritConnectionOptions($options);
622+
616623
return $this->performUpdate($values, $options);
617624
}
618625

@@ -634,6 +641,8 @@ public function increment($column, $amount = 1, array $extra = [], array $option
634641
$query->orWhereNotNull($column);
635642
});
636643

644+
$options = $this->inheritConnectionOptions($options);
645+
637646
return $this->performUpdate($query, $options);
638647
}
639648

@@ -695,7 +704,10 @@ public function delete($id = null)
695704
}
696705

697706
$wheres = $this->compileWheres();
698-
$result = $this->collection->DeleteMany($wheres);
707+
$options = $this->inheritConnectionOptions();
708+
709+
$result = $this->collection->deleteMany($wheres, $options);
710+
699711
if (1 == (int) $result->isAcknowledged()) {
700712
return $result->getDeletedCount();
701713
}
@@ -720,7 +732,8 @@ public function from($collection, $as = null)
720732
*/
721733
public function truncate(): bool
722734
{
723-
$result = $this->collection->deleteMany([]);
735+
$options = $this->inheritConnectionOptions();
736+
$result = $this->collection->deleteMany($options);
724737

725738
return 1 === (int) $result->isAcknowledged();
726739
}
@@ -853,6 +866,8 @@ protected function performUpdate($query, array $options = [])
853866
$options['multiple'] = true;
854867
}
855868

869+
$options = $this->inheritConnectionOptions($options);
870+
856871
$wheres = $this->compileWheres();
857872
$result = $this->collection->UpdateMany($wheres, $query, $options);
858873
if (1 == (int) $result->isAcknowledged()) {
@@ -1247,6 +1262,18 @@ public function options(array $options)
12471262
return $this;
12481263
}
12491264

1265+
/**
1266+
* Apply the connection's session to options if it's not already specified.
1267+
*/
1268+
private function inheritConnectionOptions(array $options = []): array
1269+
{
1270+
if (! isset($options['session']) && ($session = $this->connection->getSession())) {
1271+
$options['session'] = $session;
1272+
}
1273+
1274+
return $options;
1275+
}
1276+
12501277
/**
12511278
* @inheritdoc
12521279
*/

0 commit comments

Comments
 (0)