Skip to content

Add transaction support #1904

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

Closed
wants to merge 51 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
e54408a
add transaction support, requires mongodb version V4.0 or more and de…
klinson Jan 8, 2020
9a01d23
format code
klinson Jan 8, 2020
64b4156
add mongodb(replica set) environment into docker configuration
klinson Jan 9, 2020
34b1a98
must be compatible with Illuminate\Database\Connection::rollBack($toL…
klinson Jan 9, 2020
84850a9
add transaction usage into readme.md
klinson Jan 9, 2020
bedaaa8
update .travis.yml
klinson Jan 10, 2020
b030914
update database.conf
klinson Jan 13, 2020
163b41d
update .travis.yml
klinson Jan 13, 2020
92c1e07
update docker-compose.yml
klinson Jan 13, 2020
caa61ea
Update src/Jenssegers/Mongodb/Connection.php
klinson Jan 17, 2020
7d96c3e
fixbug Connection.php
klinson Feb 7, 2020
776492d
Merge remote-tracking branch 'remotes/upstream/master'
klinson Feb 7, 2020
10bda05
add transaction into README.md
klinson Feb 7, 2020
85a0be8
add replset mongodb tag
klinson Feb 7, 2020
b39251c
Remove travis from PR please
klinson Feb 9, 2020
fe61056
remove author tag and comments in Chinese language
klinson Feb 13, 2020
3029e36
improved function docblocks of transaction
klinson Feb 13, 2020
4ce523f
merge update
klinson Feb 15, 2020
44cb7a7
Merge remote-tracking branch 'upstream/master'
klinson Jun 28, 2020
7221417
add another testsuite for transactional tests in phpunit.xml
klinson Jun 28, 2020
1b90047
optimized code
klinson Jul 1, 2020
a5d0858
update ci and tests configs
klinson Jul 21, 2020
add9516
update tests configs
klinson Jul 21, 2020
14b3ad7
update tests configs
klinson Jul 21, 2020
0c22e4a
update ci.yml
klinson Jul 21, 2020
0b840db
update tests
klinson Jul 21, 2020
cf88a03
update tests
klinson Jul 21, 2020
2664bd5
update tests
klinson Jul 21, 2020
44bd081
update tests
klinson Jul 21, 2020
850d034
Merge branch 'master' into master
klinson Jul 21, 2020
83f45c9
delete links of docker-compose.yaml
klinson Jul 23, 2020
5eb6f42
Merge remote-tracking branch 'origin/master'
klinson Jul 23, 2020
84186f1
Merge branch 'master' into master
divine Jul 23, 2020
7741acb
optimize code
klinson Jul 23, 2020
72dbdcb
Merge remote-tracking branch 'origin/master'
klinson Jul 23, 2020
c14bd44
Merge branch 'master' into master
divine Aug 28, 2020
5773597
Merge branch 'master' into master
divine Sep 9, 2020
9e0cfd2
Merge branch 'master' into master
divine Sep 16, 2020
b2d1740
Merge branch 'master' into master
divine Oct 14, 2020
8cc01d5
Merge branch 'master' into master
klinson Nov 20, 2020
dafee61
remove testInsertWithId
klinson Nov 20, 2020
0fd27ba
update build-ci.yml
klinson Nov 23, 2020
6e89c8b
add return info to phpdoc
klinson Nov 23, 2020
5645fc7
format code
klinson Feb 19, 2021
9cd8bb2
update README.md
klinson Feb 19, 2021
551c184
Merge remote-tracking branch 'upstream/master'
klinson Feb 19, 2021
49307e2
remove excess $option
klinson Mar 27, 2021
59020fb
divide testUpdate on two methods testUpdateWithRollback and testUpda…
klinson Mar 27, 2021
97f9b4b
update ci part
klinson Mar 27, 2021
9c90125
update ci part
klinson Mar 27, 2021
041d02b
Merge remote-tracking branch 'upstream/master'
klinson Mar 27, 2021
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
95 changes: 54 additions & 41 deletions .github/workflows/build-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,44 +42,57 @@ jobs:
MYSQL_ROOT_PASSWORD:

steps:
- uses: actions/checkout@v2
- name: "Installing php"
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: curl,mbstring,xdebug
coverage: xdebug
tools: composer
- name: Show PHP version
run: php -v && composer -V
- name: Show Docker version
run: if [[ "$DEBUG" == "true" ]]; then docker version && env; fi
env:
DEBUG: ${{secrets.DEBUG}}
- name: Download Composer cache dependencies from cache
if: (!startsWith(matrix.php, '7.2'))
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache Composer dependencies
if: (!startsWith(matrix.php, '7.2'))
uses: actions/cache@v1
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ matrix.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ matrix.os }}-composer-
- name: Install dependencies
if: (!startsWith(matrix.php, '7.2'))
run: |
composer install --no-interaction
- name: Run tests
if: (!startsWith(matrix.php, '7.2'))
run: |
./vendor/bin/phpunit --coverage-clover coverage.xml
env:
MONGO_HOST: 0.0.0.0
MYSQL_HOST: 0.0.0.0
MYSQL_PORT: 3307
- uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
- uses: actions/checkout@v2
- name: Creating MongoDB replica
if: matrix.mongodb == '4.0' || matrix.mongodb == '4.2' || matrix.mongodb == '4.4'
run: |
docker run --name mongodb_repl -e MONGO_INITDB_DATABASE=unittest --publish 27018:27018 --detach mongo:${{ matrix.mongodb }} mongod --port 27018 --replSet rs
until docker exec --tty mongodb_repl mongo 127.0.0.1:27018 --eval "db.serverStatus()"; do
sleep 1
done
sudo docker exec --tty mongodb_repl mongo 127.0.0.1:27018 --eval "rs.initiate({\"_id\":\"rs\",\"members\":[{\"_id\":0,\"host\":\"127.0.0.1:27018\" }]})"
env:
MONGO_HOST: 0.0.0.0
MONGO_REPL_HOST: 0.0.0.0
- name: "Installing php"
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: curl,mbstring,xdebug
coverage: xdebug
tools: composer
- name: Show PHP version
run: php -v && composer -V
- name: Show Docker version
run: if [[ "$DEBUG" == "true" ]]; then docker version && env; fi
env:
DEBUG: ${{secrets.DEBUG}}
- name: Download Composer cache dependencies from cache
if: (!startsWith(matrix.php, '7.2'))
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache Composer dependencies
if: (!startsWith(matrix.php, '7.2'))
uses: actions/cache@v1
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ matrix.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ matrix.os }}-composer-
- name: Install dependencies
if: (!startsWith(matrix.php, '7.2'))
run: |
composer install --no-interaction
- name: Run tests
if: (!startsWith(matrix.php, '7.2'))
run: |
./vendor/bin/phpunit --coverage-clover coverage.xml
env:
MONGO_VERSION: ${{ matrix.mongodb }})
MONGO_HOST: 0.0.0.0
MONGO_REPL_HOST: 0.0.0.0
MYSQL_HOST: 0.0.0.0
MYSQL_PORT: 3307
- uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,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)
- [Transaction](#transaction)
- [Schema](#schema)
- [Basic Usage](#basic-usage-3)
- [Geospatial indexes](#geospatial-indexes)
Expand Down Expand Up @@ -968,6 +969,46 @@ 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.

Transaction
-------
Transaction requires 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

Transaction supports CREATE/INSERT/UPDATE/DELETE operations.

```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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// begin a transaction
// 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();

// you can commit your changes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// you can commit your changes
// Commit your changes

DB::commit();

// you can also rollback them
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// you can also rollback them
// Rollback them back

//DB::rollBack();
```

**NOTE:** Transaction supports infinite-level of nested transactions, but outside transaction rollbacks do not affect the commits of inside transactions.
```php
DB::beginTransaction();
User::create(['name' => 'john', 'age' => 20, 'title' => 'admin']);
DB::transaction(function () {
DB::collection('users')->where('name', 'john')->update(['age' => 20]);
});
DB::rollBack();
```

Schema
------
The database driver also has (limited) schema builder support. You can easily manipulate collections and set indexes.
Expand Down
62 changes: 60 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ services:
- .:/code
working_dir: /code
depends_on:
- mongodb
- mysql
- mongodb
- mongodb_repl
- mongodb_repl_2
- mongodb_repl_3
- mongo_repl_init
- mysql
stdin_open: true
tty: true

mysql:
container_name: mysql
Expand All @@ -32,3 +38,55 @@ services:
- 27017:27017
logging:
driver: none

mongodb_repl:
container_name: mongodb_repl
image: mongo:4.2.5
restart: always
ports:
- "27018:27018"
entrypoint: [ "/usr/bin/mongod", "--quiet", "--bind_ip_all", "--port", "27018", "--replSet", "rs" ]
depends_on:
- mongodb_repl_2
- mongodb_repl_3
logging:
driver: none

mongodb_repl_2:
container_name: mongodb_repl_2
image: mongo:4.2.5
restart: always
ports:
- "27019:27018"
entrypoint: [ "/usr/bin/mongod", "--quiet", "--bind_ip_all", "--port", "27018", "--replSet", "rs" ]
depends_on:
- mongodb_repl_3
logging:
driver: none

mongodb_repl_3:
container_name: mongodb_repl_3
image: mongo:4.2.5
restart: always
ports:
- "27020:27018"
entrypoint: [ "/usr/bin/mongod", "--quiet", "--bind_ip_all", "--port", "27018", "--replSet", "rs" ]
logging:
driver: none

mongo_repl_init:
image: mongo:4.2.5
depends_on:
- mongodb_repl
- mongodb_repl_2
- mongodb_repl_3
environment:
- MONGO1=mongodb_repl
- MONGO2=mongodb_repl_2
- MONGO3=mongodb_repl_3
- RS=rs
volumes:
- ./:/scripts
entrypoint: [ "sh", "-c", "/scripts/mongo-repl-init.sh" ]
logging:
driver: none
39 changes: 39 additions & 0 deletions mongo-replset-init.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash
mongodb1=`getent hosts ${MONGO1} | awk '{ print $1 }'`

port=${PORT:-27018}

echo "Waiting for startup.."
until mongo --host ${mongodb1}:${port} --eval 'quit(db.runCommand({ ping: 1 }).ok ? 0 : 2)' &>/dev/null; do
printf '.'
sleep 1
done

echo "Started.."

echo setup.sh time now: `date +"%T" `
mongo --host ${mongodb1}:${port} <<EOF
var cfg = {
"_id": "${RS}",
"protocolVersion": 1,
"members": [
{
"_id": 0,
"host": "${MONGO1}:${port}",
"priority": 2
},
{
"_id": 1,
"host": "${MONGO2}:${port}",
"priority": 0
},
{
"_id": 2,
"host": "${MONGO3}:${port}",
"priority": 0
}
]
};
rs.initiate(cfg, { force: true });
rs.reconfig(cfg, { force: true });
EOF
5 changes: 5 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
<file>tests/QueryBuilderTest.php</file>
<file>tests/QueryTest.php</file>
</testsuite>
<testsuite name="transaction">
<file>tests/TransactionBuilderTest.php</file>
<file>tests/TransactionTest.php</file>
</testsuite>
<testsuite name="model">
<file>tests/ModelTest.php</file>
<file>tests/RelationsTest.php</file>
Expand All @@ -36,6 +40,7 @@
</testsuites>
<php>
<env name="MONGO_HOST" value="mongodb"/>
<env name="MONGO_REPL_HOST" value="mongodb_repl"/>
<env name="MONGO_DATABASE" value="unittest"/>
<env name="MONGO_PORT" value="27017"/>
<env name="MYSQL_HOST" value="mysql"/>
Expand Down
88 changes: 88 additions & 0 deletions src/Jenssegers/Mongodb/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
use Illuminate\Support\Arr;
use InvalidArgumentException;
use MongoDB\Client;
use MongoDB\Driver\ReadConcern;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\WriteConcern;

class Connection extends BaseConnection
{
Expand All @@ -21,6 +24,17 @@ class Connection extends BaseConnection
*/
protected $connection;

/**
* A random unique id for identification of transaction.
* @var string
*/
protected $session_key;
/**
* A list of transaction sessions.
* @var array
*/
protected $sessions = [];

/**
* Create a new database connection instance.
* @param array $config
Expand Down Expand Up @@ -277,4 +291,78 @@ public function __call($method, $parameters)
{
return call_user_func_array([$this->db, $method], $parameters);
}

/**
* create a session and start a transaction in session
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* create a session and start a transaction in session
* Create a session and start a transaction in session

*
* In version 4.0, MongoDB supports multi-document transactions on replica sets.
* 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.
* To use transactions on MongoDB 4.2 deployments(replica sets and sharded clusters), clients must use MongoDB drivers updated for MongoDB 4.2.
*
* @see https://docs.mongodb.com/manual/core/transactions/
* @return void
*/
public function beginTransaction()
{
$this->session_key = uniqid();
$this->sessions[$this->session_key] = $this->connection->startSession();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that the session from the connection instance is applied automatically to all operations sent through the query builder, I'd refrain from starting multiple sessions. Looking at the transaction logic used for PDO, the first call to beginTransaction will start a new transaction, while subsequent only create save points (or no-op if those are not supported). I generally believe this to be a better alternative that should be used instead.


$this->sessions[$this->session_key]->startTransaction([
'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY),
'writeConcern' => new WriteConcern(1),
'readConcern' => new ReadConcern(ReadConcern::LOCAL)
Comment on lines +311 to +313
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid hardcoding these options this way, especially with the given values. There is no prior art for passing options (as most methods in the query builder don't accept any options), but an optional $transactionOptions parameter for this method would be more sensible than hardcoding these values here.

]);
}

/**
* commit transaction in this session and close this session
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* commit transaction in this session and close this session
* Commit transaction in this session and close this session

* @return void
*/
public function commit()
{
if ($session = $this->getSession()) {
$session->commitTransaction();
$this->setLastSession();
}
}

/**
* rollback transaction in this session and close this session
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* rollback transaction in this session and close this session
* Rollback transaction in this session and close this session

* @return void
*/
public function rollBack($toLevel = null)
{
if ($session = $this->getSession()) {
$session->abortTransaction();
$this->setLastSession();
}
}

/**
* close this session and get last session key to session_key
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* close this session and get last session key to session_key
* Close this session and get last session key to session_key

* Why do it ? Because nested transactions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Why do it ? Because nested transactions
* Required due to nested transactions

* @return void
*/
protected function setLastSession()
{
if ($session = $this->getSession()) {
$session->endSession();
unset($this->sessions[$this->session_key]);
if (empty($this->sessions)) {
$this->session_key = null;
} else {
end($this->sessions);
$this->session_key = key($this->sessions);
}
}
}

/**
* get now session if it has session
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* get now session if it has session
* Get session if it exists

* @return \MongoDB\Driver\Session|null
*/
public function getSession()
{
return $this->sessions[$this->session_key] ?? null;
}
}
Loading