Skip to content
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

Transactions support #294

Merged
merged 85 commits into from
Jul 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
db029e1
yii2 mongodb transaction
ziaratban Jan 23, 2020
9da28a2
better structure for options
ziaratban Jan 24, 2020
b2a97e1
use yii::debug instead of yii::trace
ziaratban Jan 24, 2020
9c39481
add profile to transaction
ziaratban Jan 27, 2020
2c61886
fix bug
ziaratban Feb 1, 2020
71d0b01
fix bug
ziaratban Feb 1, 2020
1acb69e
fix bug in File Collection
ziaratban Feb 11, 2020
4e94ae7
Update CHANGELOG.md
ziaratban Feb 20, 2020
d1e4da8
Update CHANGELOG.md
samdark Feb 22, 2020
ada4d4e
add Throwable in Connection::transaction()
ziaratban Mar 4, 2020
0ee20d9
support OP_ALL , OP_DELETE , OP_INSERT , OP_UPDATE
ziaratban Mar 4, 2020
50b0bb5
adding document lock feature
ziaratban Mar 4, 2020
293951f
fix bug
ziaratban Mar 4, 2020
63fd4a0
Update ActiveRecord.php
ziaratban Mar 4, 2020
6e97759
Update ActiveRecord.php
ziaratban Mar 4, 2020
01f4ad2
Update ActiveRecord.php
ziaratban Mar 5, 2020
5c7ff07
support writeConcern number parameter
ziaratban Mar 9, 2020
e21a11b
adding stubborn feature for lock a document
ziaratban Mar 9, 2020
278989e
save last session in Connection::Transaction()
ziaratban Apr 6, 2020
88e656c
Update Connection.php
ziaratban Apr 7, 2020
ab96f62
updating documents
ziaratban Apr 9, 2020
be17b90
Update Connection.php
ziaratban Apr 9, 2020
a084b8d
rename Connection::withSession() to Connection::setSession()
ziaratban Apr 9, 2020
6463da3
noTransaction feature
ziaratban Apr 10, 2020
ea65e02
adding 'return' to Connection::noTransaction()
ziaratban Apr 10, 2020
e67cebb
add exception handling for a safe return
ziaratban Apr 11, 2020
96a6d4d
Merge branch 'master' into transaction
ziaratban Jun 8, 2020
782a9d5
match with enableLogging and enableProfiling
ziaratban Jul 2, 2020
96aa4ec
cleaning
ziaratban Jul 2, 2020
0193317
fix bug
ziaratban Jul 9, 2020
3185c26
fix bug
ziaratban Oct 1, 2020
22d8c6f
fix bug
ziaratban Oct 2, 2020
4e777d9
Optimization and better understanding and fix bugs
ziaratban Oct 2, 2020
1a01b19
Update .travis.yml
ziaratban Oct 11, 2020
cf8d022
Update .travis.yml
ziaratban Oct 11, 2020
260daaf
Update .travis.yml
ziaratban Oct 11, 2020
8f8c181
Update .travis.yml
ziaratban Oct 11, 2020
1923289
Update .travis.yml
ziaratban Oct 11, 2020
fa9015a
Update .travis.yml
ziaratban Oct 11, 2020
9b9e8c0
Update .travis.yml
ziaratban Oct 11, 2020
ec360aa
Update .travis.yml
ziaratban Oct 11, 2020
5102f28
Update .travis.yml
ziaratban Oct 11, 2020
6969ebd
Update .travis.yml
ziaratban Oct 11, 2020
09d24db
Update .travis.yml
ziaratban Oct 11, 2020
084aae8
Update .travis.yml
ziaratban Oct 11, 2020
5cf4da0
Update .travis.yml
ziaratban Oct 11, 2020
df1e43c
Update .travis.yml
ziaratban Oct 11, 2020
a6e9954
Update .travis.yml
ziaratban Oct 11, 2020
d78531a
update
ziaratban Oct 11, 2020
bb77751
Update mongodb-setup.sh
ziaratban Oct 11, 2020
c62c577
update
ziaratban Oct 11, 2020
9a62685
Update .travis.yml
ziaratban Oct 11, 2020
e9231bb
Update .travis.yml
ziaratban Oct 11, 2020
6e0e6ef
Update .travis.yml
ziaratban Oct 11, 2020
c3c9653
Update .travis.yml
ziaratban Oct 11, 2020
4ee0ca9
Update .travis.yml
ziaratban Oct 12, 2020
c1dff46
adds `Once` option and modifies documents
ziaratban Oct 28, 2020
b604a33
Update Connection.php
ziaratban Oct 29, 2020
a0c575b
Merge pull request #1 from yiisoft/master
ziaratban Oct 30, 2020
0f10c08
Merge branch 'master' into transaction
ziaratban Oct 30, 2020
82e1102
Update CHANGELOG.md
samdark Oct 30, 2020
9b4173f
remove static lockField
ziaratban Apr 24, 2022
f1e5b2b
Update ActiveRecord.php
ziaratban Apr 24, 2022
0ab7973
Update src/ActiveRecord.php
ziaratban Apr 28, 2022
e95f147
Update src/ActiveRecord.php
ziaratban Apr 28, 2022
a20c162
Update src/ActiveRecord.php
ziaratban Apr 28, 2022
ccbc6c9
Update src/ActiveRecord.php
ziaratban Apr 28, 2022
ca35486
Update src/ActiveRecord.php
ziaratban Apr 28, 2022
380983b
Update src/ActiveRecord.php
ziaratban Apr 28, 2022
401eb55
Update src/ActiveRecord.php
ziaratban Apr 28, 2022
e75c4a1
Update src/ActiveRecord.php
ziaratban Apr 28, 2022
39e3286
Update src/ActiveRecord.php
ziaratban Apr 28, 2022
9e8f9f5
Update src/ActiveRecord.php
ziaratban Apr 28, 2022
8c39791
Update src/ActiveRecord.php
ziaratban Apr 28, 2022
f42ff05
Clean code & a one development
ziaratban Apr 28, 2022
fc27d61
Update src/ActiveRecord.php
ziaratban Jul 1, 2022
1534ea3
Update src/ActiveRecord.php
ziaratban Jul 1, 2022
853814b
Update src/ActiveRecord.php
ziaratban Jul 1, 2022
2f258bf
Update src/ActiveRecord.php
ziaratban Jul 1, 2022
39da9a6
Update src/Transaction.php
ziaratban Jul 1, 2022
37cdc63
Update src/Transaction.php
ziaratban Jul 1, 2022
6f607d7
Update src/Transaction.php
ziaratban Jul 1, 2022
412a8ee
Clean code & better development
ziaratban Jul 1, 2022
141a3a6
Update Collection.php
ziaratban Jul 1, 2022
49cecb1
Apply suggestions from code review
samdark Jul 1, 2022
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ Yii Framework 2 mongodb extension Change Log
2.1.10 under development
------------------------

- Enh #294: Add transactions support (ziaratban)
- Bug #308: Fix `yii\mongodb\file\Upload::addFile()` error when uploading file with readonly permissions (sparchatus)



2.1.9 November 19, 2019
-----------------------

Expand Down
259 changes: 253 additions & 6 deletions src/ActiveRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use MongoDB\BSON\Binary;
use MongoDB\BSON\Type;
use MongoDB\BSON\ObjectId;
use Yii;
use yii\base\InvalidConfigException;
use yii\db\BaseActiveRecord;
Expand All @@ -25,6 +26,27 @@
*/
abstract class ActiveRecord extends BaseActiveRecord
{
/**
* The insert operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
*/
const OP_INSERT = 0x01;

/**
* The update operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
*/
const OP_UPDATE = 0x02;

/**
* The delete operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
*/
const OP_DELETE = 0x04;

/**
* All three operations: insert, update, delete.
* This is a shortcut of the expression: OP_INSERT | OP_UPDATE | OP_DELETE.
*/
const OP_ALL = 0x07;

/**
* Returns the Mongo connection used by this AR class.
* By default, the "mongodb" application component is used as the Mongo connection.
Expand Down Expand Up @@ -208,8 +230,15 @@ public function insert($runValidation = true, $attributes = null)
if ($runValidation && !$this->validate($attributes)) {
return false;
}
$result = $this->insertInternal($attributes);

if (!$this->isTransactional(self::OP_INSERT)) {
return $this->insertInternal($attributes);
}

$result = null;
static::getDb()->transaction(function() use ($attribute, &$result) {
$result = $this->insertInternal($attributes);
});
return $result;
}

Expand Down Expand Up @@ -243,6 +272,76 @@ protected function insertInternal($attributes = null)
return true;
}

/**
* Saves the changes to this active record into the associated database table.
*
* This method performs the following steps in order:
*
* 1. call [[beforeValidate()]] when `$runValidation` is `true`. If [[beforeValidate()]]
* returns `false`, the rest of the steps will be skipped;
* 2. call [[afterValidate()]] when `$runValidation` is `true`. If validation
* failed, the rest of the steps will be skipped;
* 3. call [[beforeSave()]]. If [[beforeSave()]] returns `false`,
* the rest of the steps will be skipped;
* 4. save the record into database. If this fails, it will skip the rest of the steps;
* 5. call [[afterSave()]];
*
* In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
* [[EVENT_AFTER_VALIDATE]], [[EVENT_BEFORE_UPDATE]], and [[EVENT_AFTER_UPDATE]]
* will be raised by the corresponding methods.
*
* Only the [[dirtyAttributes|changed attribute values]] will be saved into database.
*
* For example, to update a customer record:
*
* ```php
* $customer = Customer::findOne($id);
* $customer->name = $name;
* $customer->email = $email;
* $customer->update();
* ```
*
* Note that it is possible the update does not affect any row in the table.
* In this case, this method will return 0. For this reason, you should use the following
* code to check if update() is successful or not:
*
* ```php
* if ($customer->update() !== false) {
samdark marked this conversation as resolved.
Show resolved Hide resolved
* // update successful
* } else {
* // update failed
* }
* ```
*
* @param bool $runValidation whether to perform validation (calling [[validate()]])
* before saving the record. Defaults to `true`. If the validation fails, the record
* will not be saved to the database and this method will return `false`.
* @param array $attributeNames list of attributes that need to be saved. Defaults to `null`,
* meaning all attributes that are loaded from DB will be saved.
* @return int|false the number of rows affected, or false if validation fails
* or [[beforeSave()]] stops the updating process.
* @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
* being updated is outdated.
* @throws \Exception|\Throwable in case update failed.
*/
public function update($runValidation = true, $attributeNames = null)
{
if ($runValidation && !$this->validate($attributeNames)) {
Yii::info('Model not updated due to validation error.', __METHOD__);
return false;
}

if (!$this->isTransactional(self::OP_UPDATE)) {
return $this->updateInternal($attributeNames);
}

$result = null;
static::getDb()->transaction(function() use ($attributeNames, &$result) {
$result = $this->updateInternal($attributeNames);
});
return $result;
}

/**
* @see ActiveRecord::update()
* @throws StaleObjectException
Expand Down Expand Up @@ -308,12 +407,14 @@ protected function updateInternal($attributes = null)
*/
public function delete()
{
$result = false;
if ($this->beforeDelete()) {
$result = $this->deleteInternal();
$this->afterDelete();
if (!$this->isTransactional(self::OP_DELETE)) {
return $this->deleteInternal();
}

$result = null;
static::getDb()->transaction(function() use (&$result) {
$result = $this->deleteInternal();
});
return $result;
}

Expand All @@ -323,6 +424,9 @@ public function delete()
*/
protected function deleteInternal()
{
if (!$this->beforeDelete()) {
return false;
}
// we do not check the return value of deleteAll() because it's possible
// the record is already deleted in the database and thus the method will return 0
$condition = $this->getOldPrimaryKey(true);
Expand All @@ -335,6 +439,7 @@ protected function deleteInternal()
throw new StaleObjectException('The object being deleted is outdated.');
}
$this->setOldAttributes(null);
$this->afterDelete();

return $result;
}
Expand Down Expand Up @@ -411,4 +516,146 @@ private function dumpBsonObject(Type $object)
}
return ArrayHelper::toArray($object);
}
}

/**
* Locks a document of the collection in a transaction (like `select for update` feature in MySQL)
* @see https://www.mongodb.com/blog/post/how-to-select--for-update-inside-mongodb-transactions
* @param mixed $id a document id (primary key > _id)
* @param string $lockFieldName The name of the field you want to lock.
* @param array $modifyOptions list of the options in format: optionName => optionValue.
* @param Connection $db the Mongo connection uses it to execute the query.
* @return ActiveRecord|null the locked document.
* Returns instance of ActiveRecord. Null will be returned if the query does not have a result.
*/
public static function LockDocument($id, $lockFieldName, $modifyOptions = [], $db = null)
{
$db = $db ? $db : static::getDb();
$db->transactionReady('lock document');
$options['new'] = true;
return static::find()
->where(['_id' => $id])
->modify(
[
'$set' =>[$lockFieldName => new ObjectId]
],
$modifyOptions,
$db
)
;
}

/**
* Locking a document in stubborn mode on a transaction (like `select for update` feature in MySQL)
* @see https://www.mongodb.com/blog/post/how-to-select--for-update-inside-mongodb-transactions
* notice : you can not use stubborn mode if transaction is started in current session (or use your session with `mySession` parameter).
* @param mixed $id a document id (primary key > _id)
* @param array $options list of options in format:
* [
* 'mySession' => false, # A custom session instance of ClientSession for start a transaction.
* 'transactionOptions' => [], # New transaction options. see $transactionOptions in Transaction::start()
* 'modifyOptions' => [], # See $options in ActiveQuery::modify()
* 'sleep' => 1000000, # A time parameter in microseconds to wait. the default is one second.
* 'try' => 0, # Maximum count of retry. throw write conflict error after reached this value. the zero default is unlimited.
* 'lockFieldName' => '_lock' # The name of the field you want to lock. default is '_lock'
* ]
* @param Connection $db the Mongo connection uses it to execute the query.
* @return ActiveRecord|null returns the locked document.
* Returns instance of ActiveRecord. Null will be returned if the query does not have a result.
* When the total number of attempts to lock the document passes `try`, conflict error will be thrown
*/
public static function LockDocumentStubbornly($id, $lockFieldName, $options = [], $db = null)
{
$db = $db ? $db : static::getDb();

$options = array_replace_recursive(
[
'mySession' => false,
'transactionOptions' => [],
'modifyOptions' => [],
'sleep' => 1000000,
'try' => 0,
],
$options
);

$options['modifyOptions']['new'] = true;

$session = $options['mySession'] ? $options['mySession'] : $db->startSessionOnce();

if ($session->getInTransaction()) {
throw new Exception('You can\'t use stubborn lock feature because current connection is in a transaction.');
}

// start stubborn
$tiredCounter = 0;
StartStubborn:
$session->transaction->start($options['transactionOptions']);
try {
$doc = static::find()
->where(['_id' => $id])
->modify(
[
'$set' => [
$lockFieldName => new ObjectId
]
],
$options['modifyOptions'],
$db
);
return $doc;
} catch(\Exception $e) {
$session->transaction->rollBack();
$tiredCounter++;
if ($options['try'] !== 0 && $tiredCounter === $options['try']) {
throw $e;
}
usleep($options['sleep']);
goto StartStubborn;
}
}

/**
* Declares which DB operations should be performed within a transaction in different scenarios.
* The supported DB operations are: [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]],
* which correspond to the [[insert()]], [[update()]] and [[delete()]] methods, respectively.
* By default, these methods are NOT enclosed in a DB transaction.
*
* In some scenarios, to ensure data consistency, you may want to enclose some or all of them
* in transactions. You can do so by overriding this method and returning the operations
* that need to be transactional. For example,
*
* ```php
* return [
* 'admin' => self::OP_INSERT,
* 'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
* // the above is equivalent to the following:
* // 'api' => self::OP_ALL,
*
* ];
* ```
*
* The above declaration specifies that in the "admin" scenario, the insert operation ([[insert()]])
* should be done in a transaction; and in the "api" scenario, all the operations should be done
* in a transaction.
*
* @return array the declarations of transactional operations. The array keys are scenarios names,
* and the array values are the corresponding transaction operations.
*/
public function transactions()
{
return [];
}

/**
* Returns a value indicating whether the specified operation is transactional in the current [[$scenario]].
* @param int $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]].
* @return bool whether the specified operation is transactional in the current [[scenario]].
*/
public function isTransactional($operation)
{
$scenario = $this->getScenario();
$transactions = $this->transactions();

return isset($transactions[$scenario]) && ($transactions[$scenario] & $operation);
}
}
Loading