-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Transaction support #2465
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
Transaction support #2465
Changes from 11 commits
898f4e2
e68124c
eea9943
0a2e605
203160e
8fc6915
2c549c8
a6aa3cc
2a82a54
bb58fcb
df3a53a
f2510b1
8afe105
7a644bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,10 +44,6 @@ jobs: | |
- '8.0' | ||
- '8.1' | ||
services: | ||
mongo: | ||
image: mongo:${{ matrix.mongodb }} | ||
ports: | ||
- 27017:27017 | ||
mysql: | ||
image: mysql:5.7 | ||
ports: | ||
|
@@ -59,6 +55,13 @@ jobs: | |
|
||
steps: | ||
- uses: actions/checkout@v2 | ||
- name: Create MongoDB Replica Set | ||
run: | | ||
docker run --name mongodb -p 27017:27017 -e MONGO_INITDB_DATABASE=unittest --detach mongo:${{ matrix.mongodb }} mongod --replSet rs | ||
until docker exec --tty mongodb mongo 127.0.0.1:27017 --eval "db.runCommand({ ping: 1 })"; do | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just looking at the CI output. I'm not certain, but if this line is the only thing responsible for printing the exact MongoDB server version then perhaps we should use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done - I added an additional build step for this to avoid people having to look through the entire setup output for this. |
||
sleep 1 | ||
done | ||
sudo docker exec --tty mongodb mongo 127.0.0.1:27017 --eval "rs.initiate({\"_id\":\"rs\",\"members\":[{\"_id\":0,\"host\":\"127.0.0.1:27017\" }]})" | ||
alcaeus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- name: "Installing php" | ||
uses: shivammathur/setup-php@v2 | ||
with: | ||
|
@@ -88,7 +91,7 @@ jobs: | |
run: | | ||
./vendor/bin/phpunit --coverage-clover coverage.xml | ||
env: | ||
MONGODB_URI: 'mongodb://127.0.0.1/' | ||
MONGODB_URI: 'mongodb://127.0.0.1/?replicaSet=rs' | ||
MYSQL_HOST: 0.0.0.0 | ||
MYSQL_PORT: 3307 | ||
- uses: codecov/codecov-action@v1 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,6 +37,7 @@ This package adds functionalities to the Eloquent model and Query builder for Mo | |
- [Query Builder](#query-builder) | ||
- [Basic Usage](#basic-usage-2) | ||
- [Available operations](#available-operations) | ||
- [Transactions](#transactions) | ||
- [Schema](#schema) | ||
- [Basic Usage](#basic-usage-3) | ||
- [Geospatial indexes](#geospatial-indexes) | ||
|
@@ -968,6 +969,52 @@ If you are familiar with [Eloquent Queries](http://laravel.com/docs/queries), th | |
### Available operations | ||
To see the available operations, check the [Eloquent](#eloquent) section. | ||
|
||
Transactions | ||
------------ | ||
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/) | ||
|
||
### Basic Usage | ||
|
||
```php | ||
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(); | ||
}); | ||
``` | ||
|
||
```php | ||
// 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(); | ||
|
||
// commit changes | ||
DB::commit(); | ||
``` | ||
|
||
To abort a transaction, call the `rollBack` method at any point during the transaction: | ||
```php | ||
DB::beginTransaction(); | ||
User::create(['name' => 'john', 'age' => 19, 'title' => 'admin', 'email' => '[email protected]']); | ||
|
||
// Abort the transaction, discarding any data created as part of it | ||
DB::rollBack(); | ||
``` | ||
|
||
**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) | ||
```php | ||
DB::beginTransaction(); | ||
User::create(['name' => 'john', 'age' => 20, 'title' => 'admin']); | ||
|
||
// 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(); | ||
``` | ||
|
||
Schema | ||
------ | ||
The database driver also has (limited) schema builder support. You can easily manipulate collections and set indexes. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
<?php | ||
|
||
namespace Jenssegers\Mongodb\Concerns; | ||
|
||
use Closure; | ||
use MongoDB\Client; | ||
use MongoDB\Driver\Exception\RuntimeException; | ||
use MongoDB\Driver\Session; | ||
use function MongoDB\with_transaction; | ||
use Throwable; | ||
|
||
/** | ||
* @see https://docs.mongodb.com/manual/core/transactions/ | ||
*/ | ||
trait ManagesTransactions | ||
{ | ||
protected ?Session $session = null; | ||
|
||
protected $transactions = 0; | ||
|
||
/** | ||
* @return Client | ||
*/ | ||
abstract public function getMongoClient(); | ||
|
||
public function getSession(): ?Session | ||
{ | ||
return $this->session; | ||
} | ||
|
||
private function getSessionOrCreate(): Session | ||
{ | ||
if ($this->session === null) { | ||
$this->session = $this->getMongoClient()->startSession(); | ||
} | ||
|
||
return $this->session; | ||
} | ||
|
||
private function getSessionOrThrow(): Session | ||
{ | ||
$session = $this->getSession(); | ||
|
||
if ($session === null) { | ||
throw new RuntimeException('There is no active session.'); | ||
} | ||
|
||
return $session; | ||
} | ||
|
||
/** | ||
* Starts a transaction on the active session. An active session will be created if none exists. | ||
*/ | ||
public function beginTransaction(array $options = []): void | ||
{ | ||
$this->getSessionOrCreate()->startTransaction($options); | ||
$this->transactions = 1; | ||
alcaeus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/** | ||
* Commit transaction in this session. | ||
*/ | ||
public function commit(): void | ||
{ | ||
$this->getSessionOrThrow()->commitTransaction(); | ||
$this->transactions = 0; | ||
} | ||
|
||
/** | ||
* Abort transaction in this session. | ||
*/ | ||
public function rollBack($toLevel = null): void | ||
{ | ||
$this->getSessionOrThrow()->abortTransaction(); | ||
$this->transactions = 0; | ||
} | ||
|
||
/** | ||
* Static transaction function realize the with_transaction functionality provided by MongoDB. | ||
* | ||
* @param int $attempts | ||
*/ | ||
public function transaction(Closure $callback, $attempts = 1, array $options = []): mixed | ||
{ | ||
$attemptsLeft = $attempts; | ||
$callbackResult = null; | ||
$throwable = null; | ||
|
||
$callbackFunction = function (Session $session) use ($callback, &$attemptsLeft, &$callbackResult, &$throwable) { | ||
$attemptsLeft--; | ||
|
||
if ($attemptsLeft < 0) { | ||
$session->abortTransaction(); | ||
|
||
return; | ||
} | ||
|
||
// Catch, store and re-throw any exception thrown during execution | ||
alcaeus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// of the callable. The last exception is re-thrown if the transaction | ||
// was aborted because the number of callback attempts has been exceeded. | ||
try { | ||
$callbackResult = $callback($this); | ||
} catch (Throwable $throwable) { | ||
throw $throwable; | ||
} | ||
}; | ||
|
||
with_transaction($this->getSessionOrCreate(), $callbackFunction, $options); | ||
|
||
if ($attemptsLeft < 0 && $throwable) { | ||
throw $throwable; | ||
} | ||
|
||
return $callbackResult; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remember to use
--setParameter transactionLifetimeLimitSeconds=5
(or a lower value) to minimize deadlock time in CI.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.