From 2e55af1acc427b1cdf64a748bcde34541e99dd1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lyas=20Okay?= Date: Sat, 11 Apr 2020 03:17:40 +0300 Subject: [PATCH 1/6] transaction support --- .github/workflows/build-ci.yml | 12 ++ phpunit.xml.dist | 5 + src/Jenssegers/Mongodb/Connection.php | 39 ++++++ src/Jenssegers/Mongodb/Query/Builder.php | 30 +++-- tests/TestCase.php | 1 + tests/TransactionTest.php | 159 +++++++++++++++++++++++ tests/config/database.php | 20 ++- 7 files changed, 255 insertions(+), 11 deletions(-) create mode 100644 tests/TransactionTest.php diff --git a/.github/workflows/build-ci.yml b/.github/workflows/build-ci.yml index 4728f2a8c..f9d0073ab 100644 --- a/.github/workflows/build-ci.yml +++ b/.github/workflows/build-ci.yml @@ -31,6 +31,17 @@ jobs: steps: - uses: actions/checkout@v1 + - name: Creating MongoDB replica + if: matrix.mongodb == '4.0' || matrix.mongodb == '4.2' + 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: Show PHP version run: php${{ matrix.php }} -v && composer -V - name: Show Docker version @@ -55,6 +66,7 @@ jobs: env: MONGO_HOST: 0.0.0.0 MYSQL_HOST: 0.0.0.0 + MONGO_REPL_HOST: 0.0.0.0 MYSQL_PORT: 3307 - name: Send coveralls run: vendor/bin/coveralls coverage.xml diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4da34b41d..ba883a75b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -36,6 +36,9 @@ tests/RelationsTest.php + + tests/TransactionTest.php + tests/ValidationTest.php @@ -47,6 +50,8 @@ + + diff --git a/src/Jenssegers/Mongodb/Connection.php b/src/Jenssegers/Mongodb/Connection.php index b5ba23762..ecdd26edd 100644 --- a/src/Jenssegers/Mongodb/Connection.php +++ b/src/Jenssegers/Mongodb/Connection.php @@ -9,6 +9,11 @@ class Connection extends BaseConnection { + /** + * The MongoDB session handler. + */ + protected $session; + /** * The MongoDB database handler. * @var \MongoDB\Database @@ -267,6 +272,40 @@ protected function getDefaultSchemaGrammar() return new Schema\Grammar(); } + public function beginTransaction(array $options = []) + { + if (!$this->getSession()) { + $this->session = $this->getMongoClient()->startSession(); + $this->session->startTransaction($options); + } + } + + public function commit() + { + if ($this->getSession()) { + $this->session->commitTransaction(); + $this->clearSession(); + } + } + + public function rollBack($toLevel = null) + { + if ($this->getSession()) { + $this->session->abortTransaction(); + $this->clearSession(); + } + } + + protected function clearSession() + { + $this->session = null; + } + + public function getSession() + { + return $this->session; + } + /** * Dynamically pass methods to the connection. * @param string $method diff --git a/src/Jenssegers/Mongodb/Query/Builder.php b/src/Jenssegers/Mongodb/Query/Builder.php index 7c5c973ba..a3f697265 100644 --- a/src/Jenssegers/Mongodb/Query/Builder.php +++ b/src/Jenssegers/Mongodb/Query/Builder.php @@ -562,8 +562,10 @@ public function insert(array $values) $values = [$values]; } + $options = $this->session(); + // Batch insert - $result = $this->collection->insertMany($values); + $result = $this->collection->insertMany($values, $options); return (1 == (int) $result->isAcknowledged()); } @@ -573,7 +575,9 @@ public function insert(array $values) */ public function insertGetId(array $values, $sequence = null) { - $result = $this->collection->insertOne($values); + $options = $this->session(); + + $result = $this->collection->insertOne($values, $options); if (1 == (int) $result->isAcknowledged()) { if ($sequence === null) { @@ -595,7 +599,7 @@ public function update(array $values, array $options = []) $values = ['$set' => $values]; } - return $this->performUpdate($values, $options); + return $this->performUpdate($values, array_merge($options, $this->session())); } /** @@ -616,7 +620,7 @@ public function increment($column, $amount = 1, array $extra = [], array $option $query->orWhereNotNull($column); }); - return $this->performUpdate($query, $options); + return $this->performUpdate($query, array_merge($options, $this->session())); } /** @@ -624,7 +628,7 @@ public function increment($column, $amount = 1, array $extra = [], array $option */ public function decrement($column, $amount = 1, array $extra = [], array $options = []) { - return $this->increment($column, -1 * $amount, $extra, $options); + return $this->increment($column, -1 * $amount, $extra, array_merge($options, $this->session())); } /** @@ -674,8 +678,9 @@ public function delete($id = null) $this->where('_id', '=', $id); } + $options = $this->session(); $wheres = $this->compileWheres(); - $result = $this->collection->DeleteMany($wheres); + $result = $this->collection->DeleteMany($wheres, $options); if (1 == (int) $result->isAcknowledged()) { return $result->getDeletedCount(); } @@ -704,7 +709,7 @@ public function truncate() 'typeMap' => ['root' => 'object', 'document' => 'object'], ]; - $result = $this->collection->drop($options); + $result = $this->collection->drop(array_merge($options, $this->session())); return (1 == (int) $result->ok); } @@ -833,7 +838,7 @@ protected function performUpdate($query, array $options = []) } $wheres = $this->compileWheres(); - $result = $this->collection->UpdateMany($wheres, $query, $options); + $result = $this->collection->UpdateMany($wheres, $query, array_merge($options, $this->session())); if (1 == (int) $result->isAcknowledged()) { return $result->getModifiedCount() ? $result->getModifiedCount() : $result->getUpsertedCount(); } @@ -1153,6 +1158,15 @@ public function options(array $options) return $this; } + protected function session(array $options = []) + { + if ($session = $this->connection->getSession()) { + $options['session'] = $this->connection->getSession(); + } + + return $options; + } + /** * @inheritdoc */ diff --git a/tests/TestCase.php b/tests/TestCase.php index 4c01d5755..0417665c3 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -51,6 +51,7 @@ protected function getEnvironmentSetUp($app) $app['config']->set('database.default', 'mongodb'); $app['config']->set('database.connections.mysql', $config['connections']['mysql']); $app['config']->set('database.connections.mongodb', $config['connections']['mongodb']); + $app['config']->set('database.connections.mongodb_repl', $config['connections']['mongodb_repl']); $app['config']->set('database.connections.mongodb2', $config['connections']['mongodb']); $app['config']->set('database.connections.dsn_mongodb', $config['connections']['dsn_mongodb']); $app['config']->set('database.connections.dsn_mongodb_db', $config['connections']['dsn_mongodb_db']); diff --git a/tests/TransactionTest.php b/tests/TransactionTest.php new file mode 100644 index 000000000..714fe8354 --- /dev/null +++ b/tests/TransactionTest.php @@ -0,0 +1,159 @@ +markTestSkipped('MongoDB with version below 4 is not supported for transactions'); + } + + $config = require 'config/database.php'; + + $app['config']->set('database.connections.'.$this->connection, $config['connections'][$this->connection]); + $app['config']->set('database.default', $this->connection); + } + + public function tearDown(): void + { + parent::setUp(); + + User::on($this->connection)->truncate(); + DB::collection('users')->truncate(); + Schema::drop('users'); + } + + public function testCommitTransaction(): void + { + /** + * Insert Commit + */ + try { + DB::beginTransaction(); + + User::on($this->connection)->insert([ + 'name' => 'John Doe' + ]); + + DB::commit(); + } catch (Exception $e) { + DB::rollBack(); + + $this->assertTrue(false); + } + + $this->assertTrue(User::on($this->connection)->where('name', 'John Doe')->exists()); + + /** + * Update Commit + */ + try { + DB::beginTransaction(); + + User::on($this->connection)->where('name', 'John Doe')->update([ + 'name' => 'Jane Doe' + ]); + + DB::commit(); + } catch (Exception $e) { + DB::rollBack(); + + $this->assertTrue(false); + } + + $this->assertTrue(User::on($this->connection)->where('name', 'Jane Doe')->exists()); + + /** + * Delete Commit + */ + try { + DB::beginTransaction(); + + User::on($this->connection)->where('name', 'Jane Doe')->delete(); + + DB::commit(); + } catch (Exception $e) { + DB::rollBack(); + + $this->assertTrue(false); + } + + $this->assertFalse(User::on($this->connection)->where('name', 'Jane Doe')->exists()); + } + + public function testRollbackTransaction(): void + { + try { + DB::beginTransaction(); + + User::on($this->connection)->insert([ + 'name' => 'John Doe' + ]); + + DB::rollBack(); + } catch (Exception $e) { + DB::rollBack(); + + $this->assertTrue(false); + } + + $this->assertFalse(User::on($this->connection)->where('name', 'John Doe')->exists()); + + try { + DB::beginTransaction(); + + User::on($this->connection)->insert([ + 'name' => 'John Doe' + ]); + + DB::commit(); + } catch (Exception $e) { + DB::rollBack(); + + $this->assertTrue(false); + } + + $this->assertTrue(User::on($this->connection)->where('name', 'John Doe')->exists()); + + try { + DB::beginTransaction(); + + User::on($this->connection)->where('name', 'John Doe')->update([ + 'name' => 'Jane Doe' + ]); + + DB::rollBack(); + } catch (Exception $e) { + DB::rollBack(); + + $this->assertTrue(false); + } + + $this->assertTrue(User::on($this->connection)->where('name', 'John Doe')->exists()); + + try { + DB::beginTransaction(); + + User::on($this->connection)->where('name', 'John Doe')->delete(); + + DB::rollBack(); + } catch (Exception $e) { + DB::rollBack(); + + $this->assertTrue(false); + } + + $this->assertTrue(User::on($this->connection)->where('name', 'John Doe')->exists()); + } +} diff --git a/tests/config/database.php b/tests/config/database.php index 556b71d33..94ba6916a 100644 --- a/tests/config/database.php +++ b/tests/config/database.php @@ -1,8 +1,10 @@ env('MONGO_DATABASE', 'unittest'), ], + 'mongodb_repl' => [ + 'name' => 'mongodb_repl', + 'driver' => 'mongodb', + 'host' => $mongoReplHost, + 'port' => $mongoReplPort, + 'database' => env('MONGO_DATABASE', 'unittest'), + 'options' => [ + 'replicaSet' => 'rs', + 'serverSelectionTryOnce' => false, + ], + ], + 'dsn_mongodb' => [ 'driver' => 'mongodb', 'dsn' => "mongodb://$mongoHost:$mongoPort", From 54476dcc065046befa903d457f29ae65e9d56a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lyas=20Okay?= Date: Sat, 11 Apr 2020 03:27:25 +0300 Subject: [PATCH 2/6] transaction support --- .github/workflows/build-ci.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-ci.yml b/.github/workflows/build-ci.yml index f9d0073ab..cc9b40f4f 100644 --- a/.github/workflows/build-ci.yml +++ b/.github/workflows/build-ci.yml @@ -28,18 +28,17 @@ jobs: MYSQL_DATABASE: 'unittest' MYSQL_ROOT_PASSWORD: name: PHP v${{ matrix.php }} with Mongo v${{ matrix.mongodb }} - steps: - uses: actions/checkout@v1 - name: Creating MongoDB replica - if: matrix.mongodb == '4.0' || matrix.mongodb == '4.2' - run: | + if: matrix.mongodb == '4.0' || matrix.mongodb == '4.2' + 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: + env: MONGO_HOST: 0.0.0.0 MONGO_REPL_HOST: 0.0.0.0 - name: Show PHP version @@ -66,7 +65,6 @@ jobs: env: MONGO_HOST: 0.0.0.0 MYSQL_HOST: 0.0.0.0 - MONGO_REPL_HOST: 0.0.0.0 MYSQL_PORT: 3307 - name: Send coveralls run: vendor/bin/coveralls coverage.xml From d8344b5159b45a1f52a3d2c03029c0b5c0f1d1ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lyas=20Okay?= Date: Sat, 11 Apr 2020 03:44:44 +0300 Subject: [PATCH 3/6] transaction support --- .github/workflows/build-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-ci.yml b/.github/workflows/build-ci.yml index cc9b40f4f..020e6702c 100644 --- a/.github/workflows/build-ci.yml +++ b/.github/workflows/build-ci.yml @@ -63,7 +63,9 @@ jobs: 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 - name: Send coveralls From e590578658a7027632e057b769e65a6998fc06a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lyas=20Okay?= Date: Sat, 11 Apr 2020 03:45:28 +0300 Subject: [PATCH 4/6] transaction support --- .github/workflows/build-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-ci.yml b/.github/workflows/build-ci.yml index 020e6702c..e2ff26b47 100644 --- a/.github/workflows/build-ci.yml +++ b/.github/workflows/build-ci.yml @@ -65,7 +65,7 @@ jobs: env: MONGO_VERSION: ${{ matrix.mongodb }}) MONGO_HOST: 0.0.0.0 - MONGO_REPL_HOST: 0.0.0.0 + MONGO_REPL_HOST: 0.0.0.0 MYSQL_HOST: 0.0.0.0 MYSQL_PORT: 3307 - name: Send coveralls From 643e4d4b9a08e9daae54f876e9cafe794be5e30f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lyas=20Okay?= Date: Wed, 15 Apr 2020 22:54:02 +0300 Subject: [PATCH 5/6] creating namespace for multi-document transaction --- src/Jenssegers/Mongodb/Query/Builder.php | 10 ++++++++++ tests/TransactionTest.php | 2 -- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Jenssegers/Mongodb/Query/Builder.php b/src/Jenssegers/Mongodb/Query/Builder.php index a3f697265..1451155b7 100644 --- a/src/Jenssegers/Mongodb/Query/Builder.php +++ b/src/Jenssegers/Mongodb/Query/Builder.php @@ -564,6 +564,16 @@ public function insert(array $values) $options = $this->session(); + // Check if table exists for multi-document transaction, otherwise create table. + if (!empty($options)) { + $schemaBuilder = $this->connection->getSchemaBuilder(); + $tableName = $this->collection->getCollectionName(); + + if (!$schemaBuilder->hasTable($tableName)) { + $schemaBuilder->create($tableName); + } + } + // Batch insert $result = $this->collection->insertMany($values, $options); diff --git a/tests/TransactionTest.php b/tests/TransactionTest.php index 714fe8354..dcfd3bfa4 100644 --- a/tests/TransactionTest.php +++ b/tests/TransactionTest.php @@ -9,8 +9,6 @@ class TransactionTest extends TestCase public function setUp(): void { parent::setUp(); - - Schema::create('users'); } protected function getEnvironmentSetUp($app) From 7cbfa84bd95adfdfa96f74465c9da5e1a69e35fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lyas=20Okay?= Date: Thu, 16 Apr 2020 01:47:59 +0300 Subject: [PATCH 6/6] docker compose reconfiguration for mongodb replica set --- docker-compose.yml | 67 +++++++++++++++++++++++++++++++++++++-- mongo-repl-init.sh | 39 +++++++++++++++++++++++ tests/TransactionTest.php | 1 + 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 mongo-repl-init.sh diff --git a/docker-compose.yml b/docker-compose.yml index c6f20163e..c5ca864b6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,8 +10,12 @@ services: - .:/code working_dir: /code depends_on: - - mongodb - - mysql + - mongodb + - mongodb_repl + - mongodb_repl_2 + - mongodb_repl_3 + - mongo_repl_init + - mysql mysql: container_name: mysql @@ -30,3 +34,62 @@ 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 + links: + - mongodb_repl_2:mongodb_repl_2 + - mongodb_repl_3: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 + links: + - mongodb_repl:mongodb_repl + - mongodb_repl_2:mongodb_repl_2 + - mongodb_repl_3: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 diff --git a/mongo-repl-init.sh b/mongo-repl-init.sh new file mode 100644 index 000000000..9c184d51a --- /dev/null +++ b/mongo-repl-init.sh @@ -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} <