Skip to content

Commit 898f4e2

Browse files
alcaeusklinsonlevon80999
committed
Add support for transactions
Co-authored-by: klinson <klinson@163.com> Co-authored-by: levon80999 <levonb@ucraft.com>
1 parent 0606fc0 commit 898f4e2

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,11 +5,14 @@
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
use MongoDB\Database;
1011

1112
class Connection extends BaseConnection
1213
{
14+
use ManagesTransactions;
15+
1316
/**
1417
* The MongoDB database handler.
1518
*

src/Query/Builder.php

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

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

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

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

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

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

@@ -581,8 +583,9 @@ public function insert(array $values)
581583
$values = [$values];
582584
}
583585

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

587590
return 1 == (int) $result->isAcknowledged();
588591
}
@@ -592,7 +595,9 @@ public function insert(array $values)
592595
*/
593596
public function insertGetId(array $values, $sequence = null)
594597
{
595-
$result = $this->collection->insertOne($values);
598+
$options = $this->inheritConnectionOptions();
599+
600+
$result = $this->collection->insertOne($values, $options);
596601

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

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

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

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

@@ -696,7 +705,10 @@ public function delete($id = null)
696705
}
697706

698707
$wheres = $this->compileWheres();
699-
$result = $this->collection->DeleteMany($wheres);
708+
$options = $this->inheritConnectionOptions();
709+
710+
$result = $this->collection->deleteMany($wheres, $options);
711+
700712
if (1 == (int) $result->isAcknowledged()) {
701713
return $result->getDeletedCount();
702714
}
@@ -721,7 +733,8 @@ public function from($collection, $as = null)
721733
*/
722734
public function truncate(): bool
723735
{
724-
$result = $this->collection->deleteMany([]);
736+
$options = $this->inheritConnectionOptions();
737+
$result = $this->collection->deleteMany($options);
725738

726739
return 1 === (int) $result->isAcknowledged();
727740
}
@@ -855,6 +868,8 @@ protected function performUpdate($query, array $options = [])
855868
$options['multiple'] = true;
856869
}
857870

871+
$options = $this->inheritConnectionOptions($options);
872+
858873
$wheres = $this->compileWheres();
859874
$result = $this->collection->UpdateMany($wheres, $query, $options);
860875
if (1 == (int) $result->isAcknowledged()) {
@@ -1249,6 +1264,18 @@ public function options(array $options)
12491264
return $this;
12501265
}
12511266

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

0 commit comments

Comments
 (0)