diff --git a/.github/workflows/build-ci.yml b/.github/workflows/build-ci.yml index a4d32a34e..a68c051ca 100644 --- a/.github/workflows/build-ci.yml +++ b/.github/workflows/build-ci.yml @@ -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 diff --git a/README.md b/README.md index 808e266ec..0403c0016 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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' => 'klinsonup@gmail.com']); + 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' => 'klinsonup@gmail.com']); +DB::collection('users')->where('name', 'john')->update(['age' => 20]); +DB::collection('users')->where('name', 'john')->delete(); + +// you can commit your changes +DB::commit(); + +// you can also rollback them +//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. diff --git a/docker-compose.yml b/docker-compose.yml index ec612f1fe..8f38d705d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 @@ -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 diff --git a/mongo-replset-init.sh b/mongo-replset-init.sh new file mode 100644 index 000000000..9c184d51a --- /dev/null +++ b/mongo-replset-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} <tests/QueryBuilderTest.php tests/QueryTest.php + + tests/TransactionBuilderTest.php + tests/TransactionTest.php + tests/ModelTest.php tests/RelationsTest.php @@ -36,6 +40,7 @@ + diff --git a/src/Jenssegers/Mongodb/Connection.php b/src/Jenssegers/Mongodb/Connection.php index b5ba23762..641eb80c2 100644 --- a/src/Jenssegers/Mongodb/Connection.php +++ b/src/Jenssegers/Mongodb/Connection.php @@ -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 { @@ -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 @@ -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 + * + * 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(); + + $this->sessions[$this->session_key]->startTransaction([ + 'readPreference' => new ReadPreference(ReadPreference::RP_PRIMARY), + 'writeConcern' => new WriteConcern(1), + 'readConcern' => new ReadConcern(ReadConcern::LOCAL) + ]); + } + + /** + * 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 + * @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 + * Why do it ? Because 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 + * @return \MongoDB\Driver\Session|null + */ + public function getSession() + { + return $this->sessions[$this->session_key] ?? null; + } } diff --git a/src/Jenssegers/Mongodb/Query/Builder.php b/src/Jenssegers/Mongodb/Query/Builder.php index afe58e108..a4335dde4 100644 --- a/src/Jenssegers/Mongodb/Query/Builder.php +++ b/src/Jenssegers/Mongodb/Query/Builder.php @@ -334,6 +334,9 @@ public function getFresh($columns = [], $returnLazy = false) $options = array_merge($options, $this->options); } + // if transaction in session + $options = $this->setSession($options); + // Execute aggregation $results = iterator_to_array($this->collection->aggregate($pipeline, $options)); @@ -344,12 +347,12 @@ public function getFresh($columns = [], $returnLazy = false) // Return distinct results directly $column = isset($this->columns[0]) ? $this->columns[0] : '_id'; + $options = []; + // if transaction in session + $options = $this->setSession($options); + // Execute distinct - if ($wheres) { - $result = $this->collection->distinct($column, $wheres); - } else { - $result = $this->collection->distinct($column); - } + $result = $this->collection->distinct($column, $wheres ?: [], $options); return new Collection($result); } // Normal query @@ -395,6 +398,9 @@ public function getFresh($columns = [], $returnLazy = false) $options = array_merge($options, $this->options); } + // if transaction in session + $options = $this->setSession($options); + // Execute query and get MongoCursor $cursor = $this->collection->find($wheres, $options); @@ -566,8 +572,10 @@ public function insert(array $values) $values = [$values]; } - // Batch insert - $result = $this->collection->insertMany($values); + // if transaction in session + $options = $this->setSession(); + + $result = $this->collection->insertMany($values, $options); return (1 == (int) $result->isAcknowledged()); } @@ -577,7 +585,10 @@ public function insert(array $values) */ public function insertGetId(array $values, $sequence = null) { - $result = $this->collection->insertOne($values); + // if transaction in session + $options = $this->setSession(); + + $result = $this->collection->insertOne($values, $options); if (1 == (int) $result->isAcknowledged()) { if ($sequence === null) { @@ -598,6 +609,8 @@ public function update(array $values, array $options = []) if (!Str::startsWith(key($values), '$')) { $values = ['$set' => $values]; } + // if transaction in session + $options = $this->setSession($options); return $this->performUpdate($values, $options); } @@ -620,6 +633,9 @@ public function increment($column, $amount = 1, array $extra = [], array $option $query->orWhereNotNull($column); }); + // if transaction in session + $options = $this->setSession($options); + return $this->performUpdate($query, $options); } @@ -679,7 +695,11 @@ public function delete($id = null) } $wheres = $this->compileWheres(); - $result = $this->collection->DeleteMany($wheres); + + // if transaction in session + $options = $this->setSession(); + + $result = $this->collection->DeleteMany($wheres, $options); if (1 == (int) $result->isAcknowledged()) { return $result->getDeletedCount(); } @@ -704,7 +724,10 @@ public function from($collection, $as = null) */ public function truncate(): bool { - $result = $this->collection->deleteMany([]); + // Check if transaction exist in session + $options = $this->setSession(); + + $result = $this->collection->deleteMany($options); return (1 === (int) $result->isAcknowledged()); } @@ -832,6 +855,9 @@ protected function performUpdate($query, array $options = []) $options['multiple'] = true; } + // Check if transaction exist in session + $options = $this->setSession($options); + $wheres = $this->compileWheres(); $result = $this->collection->UpdateMany($wheres, $query, $options); if (1 == (int) $result->isAcknowledged()) { @@ -1153,6 +1179,19 @@ public function options(array $options) return $this; } + /** + * Set session for the transaction + * @param $session + * @return mixed + */ + protected function setSession($options = []) + { + if (!isset($options['session']) && ($session = $this->connection->getSession())) { + $options['session'] = $session; + } + return $options; + } + /** * @inheritdoc */ diff --git a/tests/TestCase.php b/tests/TestCase.php index 4c01d5755..73c07daf6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -54,6 +54,7 @@ protected function getEnvironmentSetUp($app) $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']); + $app['config']->set('database.connections.mongodb_repl', $config['connections']['mongodb_repl']); $app['config']->set('auth.model', 'User'); $app['config']->set('auth.providers.users.model', 'User'); diff --git a/tests/TransactionBuilderTest.php b/tests/TransactionBuilderTest.php new file mode 100644 index 000000000..eba7a5a93 --- /dev/null +++ b/tests/TransactionBuilderTest.php @@ -0,0 +1,220 @@ + 'klinson', 'age' => 20, 'title' => 'admin']; + protected $originData = ['name' => 'users', 'age' => 20, 'title' => 'user']; + protected $connection = 'mongodb_repl'; + protected $originConnection = 'mongodb'; + + public function setUp(): void + { + parent::setUp(); + + /** change connection to seplset? because the transaction needs */ + $this->originConnection = DB::getDefaultConnection(); + DB::setDefaultConnection($this->connection); + + DB::collection('users')->truncate(); + DB::collection('users')->insert($this->originData); + } + + protected function getEnvironmentSetUp($app) + { + if (version_compare(env('MONGO_VERSION'), '4', '<')) { + $this->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 + { + DB::collection('users')->truncate(); + DB::setDefaultConnection($this->originConnection); + parent::tearDown(); + } + + public function testInsert() + { + /** rollback test */ + DB::beginTransaction(); + DB::collection('users')->insert($this->insertData); + DB::rollBack(); + $users = DB::collection('users')->where('name', $this->insertData['name'])->get(); + $this->assertCount(0, $users); + + /** commit test */ + DB::beginTransaction(); + DB::collection('users')->insert($this->insertData); + DB::commit(); + $users = DB::collection('users')->where('name', $this->insertData['name'])->get(); + $this->assertCount(1, $users); + } + + public function testInsertGetId() + { + /** rollback test */ + DB::beginTransaction(); + $user_id = DB::collection('users')->insertGetId($this->insertData); + DB::rollBack(); + $user_id = (string) $user_id; + $user = DB::collection('users')->find($user_id); + $this->assertNull($user); + + /** commit test */ + DB::beginTransaction(); + $user_id = DB::collection('users')->insertGetId($this->insertData); + DB::commit(); + $user_id = (string) $user_id; + $user = DB::collection('users')->find($user_id); + $this->assertEquals($this->insertData['name'], $user['name']); + } + + public function testUpdate() + { + /** rollback test */ + $new_age = $this->originData['age'] + 1; + DB::beginTransaction(); + DB::collection('users')->where('name', $this->originData['name'])->update(['age' => $new_age]); + DB::rollBack(); + $users = DB::collection('users')->where('name', $this->originData['name'])->where('age', $new_age)->get(); + $this->assertCount(0, $users); + + /** commit test */ + DB::beginTransaction(); + DB::collection('users')->where('name', $this->originData['name'])->update(['age' => $new_age]); + DB::commit(); + $users = DB::collection('users')->where('name', $this->originData['name'])->where('age', $new_age)->get(); + $this->assertCount(1, $users); + } + + public function testDelete() + { + /** rollback test */ + DB::beginTransaction(); + DB::collection('users')->where('name', $this->originData['name'])->delete(); + DB::rollBack(); + $users = DB::collection('users')->where('name', $this->originData['name'])->get(); + $this->assertCount(1, $users); + + /** commit test */ + DB::beginTransaction(); + DB::collection('users')->where('name', $this->originData['name'])->delete(); + DB::commit(); + $users = DB::collection('users')->where('name', $this->originData['name'])->get(); + $this->assertCount(0, $users); + } + + public function testIncrement() + { + /** rollback test */ + DB::beginTransaction(); + DB::collection('users')->where('name', $this->originData['name'])->increment('age'); + DB::rollBack(); + $user = DB::collection('users')->where('name', $this->originData['name'])->first(); + $this->assertEquals($this->originData['age'], $user['age']); + + /** commit test */ + DB::beginTransaction(); + DB::collection('users')->where('name', $this->originData['name'])->increment('age'); + DB::commit(); + $user = DB::collection('users')->where('name', $this->originData['name'])->first(); + $this->assertEquals($this->originData['age'] + 1, $user['age']); + } + + public function testDecrement() + { + /** rollback test */ + DB::beginTransaction(); + DB::collection('users')->where('name', $this->originData['name'])->decrement('age'); + DB::rollBack(); + $user = DB::collection('users')->where('name', $this->originData['name'])->first(); + $this->assertEquals($this->originData['age'], $user['age']); + + /** commit test */ + DB::beginTransaction(); + DB::collection('users')->where('name', $this->originData['name'])->decrement('age'); + DB::commit(); + $user = DB::collection('users')->where('name', $this->originData['name'])->first(); + $this->assertEquals($this->originData['age'] - 1, $user['age']); + } + + public function testQuery() + { + /** rollback test */ + DB::beginTransaction(); + $count = DB::collection('users')->count(); + $this->assertEquals(1, $count); + DB::collection('users')->insert($this->insertData); + $count = DB::collection('users')->count(); + $this->assertEquals(2, $count); + DB::rollBack(); + $count = DB::collection('users')->count(); + $this->assertEquals(1, $count); + + /** commit test */ + DB::beginTransaction(); + $count = DB::collection('users')->count(); + $this->assertEquals(1, $count); + DB::collection('users')->insert($this->insertData); + $count = DB::collection('users')->count(); + $this->assertEquals(2, $count); + DB::commit(); + $count = DB::collection('users')->count(); + $this->assertEquals(2, $count); + } + + public function testTransaction() + { + $count = DB::collection('users')->count(); + $this->assertEquals(1, $count); + + $new_age = $this->originData['age'] + 1; + DB::transaction(function () use ($new_age) { + DB::collection('users')->insert($this->insertData); + DB::collection('users')->where('name', $this->originData['name'])->update(['age' => $new_age]); + }); + $count = DB::collection('users')->count(); + $this->assertEquals(2, $count); + + $checkInsert = DB::collection('users')->where('name', $this->insertData['name'])->first(); + $this->assertNotNull($checkInsert); + $this->assertEquals($this->insertData['age'], $checkInsert['age']); + + $checkUpdate = DB::collection('users')->where('name', $this->originData['name'])->first(); + $this->assertEquals($new_age, $checkUpdate['age']); + } + + /** + * Supports infinite-level nested transactions, but outside transaction rollbacks do not affect the commit of inside transactions + */ + public function TestNestingTransaction() + { + DB::collection('users')->insert($this->insertData); + $new_age1 = $this->originData['age'] + 1; + $new_age2 = $this->insertData['age'] + 1; + /** outside transaction */ + DB::beginTransaction(); + + DB::collection('users')->where('name', $this->originData['name'])->update(['age' => $new_age1]); + + /** inside transaction */ + DB::transaction(function () use ($new_age2) { + DB::collection('users')->where('name', $this->insertData['name'])->update(['age' => $new_age2]); + }); + + DB::rollBack(); + + $check1 = DB::collection('users')->where('name', $this->originData['name'])->first(); + $check2 = DB::collection('users')->where('name', $this->insertData['name'])->first(); + $this->assertNotEquals($new_age1, $check1['age']); + $this->assertEquals($new_age2, $check2['age']); + } +} diff --git a/tests/TransactionTest.php b/tests/TransactionTest.php new file mode 100644 index 000000000..b9ad78bf2 --- /dev/null +++ b/tests/TransactionTest.php @@ -0,0 +1,183 @@ + 'klinson', 'age' => 20, 'title' => 'admin']; + protected $originData = ['name' => 'users', 'age' => 20, 'title' => 'user']; + protected $connection = 'mongodb_repl'; + + public function setUp(): void + { + parent::setUp(); + + User::on($this->connection)->truncate(); + User::on($this->connection)->create($this->originData); + } + + protected function getEnvironmentSetUp($app) + { + if (version_compare(env('MONGO_VERSION'), '4', '<')) { + $this->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 + { + User::on($this->connection)->truncate(); + + parent::tearDown(); + } + + public function testCreate() + { + /** rollback test */ + DB::beginTransaction(); + $user = User::on($this->connection)->create($this->insertData); + DB::rollBack(); + $this->assertInstanceOf(\Jenssegers\Mongodb\Eloquent\Model::class, $user); + $this->assertTrue($user->exists); + $this->assertEquals($this->insertData['name'], $user->name); + + $check = User::on($this->connection)->find($user->_id); + $this->assertNull($check); + + /** commit test */ + DB::beginTransaction(); + $user = User::on($this->connection)->create($this->insertData); + DB::commit(); + $this->assertInstanceOf(\Jenssegers\Mongodb\Eloquent\Model::class, $user); + $this->assertTrue($user->exists); + $this->assertEquals($this->insertData['name'], $user->name); + + $check = User::on($this->connection)->find($user->_id); + $this->assertNotNull($check); + $this->assertEquals($user->name, $check->name); + } + + public function testInsert() + { + /** rollback test */ + DB::beginTransaction(); + $user = User::on($this->connection)->getModel(); + $user->name = $this->insertData['name']; + $user->title = $this->insertData['title']; + $user->age = $this->insertData['age']; + $user->save(); + DB::rollBack(); + + $this->assertTrue($user->exists); + $this->assertTrue(isset($user->_id)); + $this->assertIsString($user->_id); + + $check = User::on($this->connection)->find($user->_id); + $this->assertNull($check); + + /** commit test */ + DB::beginTransaction(); + $user = User::on($this->connection)->getModel(); + $user->name = $this->insertData['name']; + $user->title = $this->insertData['title']; + $user->age = $this->insertData['age']; + $user->save(); + DB::commit(); + + $this->assertTrue($user->exists); + $this->assertTrue(isset($user->_id)); + $this->assertIsString($user->_id); + + $check = User::on($this->connection)->find($user->_id); + $this->assertNotNull($check); + $this->assertEquals($check->name, $user->name); + $this->assertEquals($check->age, $user->age); + $this->assertEquals($check->title, $user->title); + } + + public function testUpdateWithRollback () + { + /** rollback test */ + $new_age = $this->insertData['age'] + 1; + $user1 = User::on($this->connection)->create($this->insertData); + $user2 = User::on($this->connection)->create($this->insertData); + DB::beginTransaction(); + $user1->age = $new_age; + $user1->update(); + $user2->update(['age' => $new_age]); + DB::rollBack(); + $this->assertEquals($new_age, $user1->age); + $this->assertEquals($new_age, $user2->age); + + $check1 = User::on($this->connection)->find($user1->_id); + $check2 = User::on($this->connection)->find($user2->_id); + $this->assertEquals($this->insertData['age'], $check1->age); + $this->assertEquals($this->insertData['age'], $check2->age); + } + + public function testUpdateWithCommit() + { + $new_age = $this->insertData['age'] + 1; + + /** commit test */ + User::on($this->connection)->truncate(); + $user1 = User::on($this->connection)->create($this->insertData); + $user2 = User::on($this->connection)->create($this->insertData); + DB::beginTransaction(); + $user1->age = $new_age; + $user1->update(); + $user2->update(['age' => $new_age]); + DB::commit(); + $this->assertEquals($new_age, $user1->age); + $this->assertEquals($new_age, $user2->age); + + $check1 = User::on($this->connection)->find($user1->_id); + $check2 = User::on($this->connection)->find($user2->_id); + $this->assertEquals($new_age, $check1->age); + $this->assertEquals($new_age, $check2->age); + } + + public function testDelete() + { + /** rollback test */ + $user1 = User::on($this->connection)->create($this->insertData); + DB::beginTransaction(); + $user1->delete(); + DB::rollBack(); + $check1 = User::on($this->connection)->find($user1->_id); + $this->assertNotNull($check1); + + /** commit test */ + User::on($this->connection)->truncate(); + $user1 = User::on($this->connection)->create($this->insertData); + DB::beginTransaction(); + $user1->delete(); + DB::commit(); + $check1 = User::on($this->connection)->find($user1->_id); + $this->assertNull($check1); + } + + public function testTransaction() + { + $count = User::on($this->connection)->count(); + $this->assertEquals(1, $count); + $new_age = $this->originData['age'] + 1; + DB::transaction(function () use ($new_age) { + User::on($this->connection)->create($this->insertData); + User::on($this->connection)->where($this->originData['name'])->update(['age' => $new_age]); + }); + $count = User::on($this->connection)->count(); + $this->assertEquals(2, $count); + + $checkInsert = User::on($this->connection)->where($this->insertData['name'])->first(); + $this->assertNotNull($checkInsert); + + $checkUpdate = User::on($this->connection)->where($this->originData['name'])->first(); + $this->assertEquals($new_age, $checkUpdate->age); + } +} diff --git a/tests/config/database.php b/tests/config/database.php index 556b71d33..1a61d855c 100644 --- a/tests/config/database.php +++ b/tests/config/database.php @@ -1,7 +1,9 @@ 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",