mirror of
https://github.com/yiisoft/yii2.git
synced 2025-11-16 06:17:56 +08:00
Merge pull request #253 from resurtm/ar-transactions-3
Fixes #226: atomic operations and transaction support in AR.
This commit is contained in:
@@ -446,3 +446,7 @@ $customers = Customer::find()->olderThan(50)->all();
|
|||||||
|
|
||||||
The parameters should follow after the `$query` parameter when defining the scope method, and they
|
The parameters should follow after the `$query` parameter when defining the scope method, and they
|
||||||
can take default values like shown above.
|
can take default values like shown above.
|
||||||
|
|
||||||
|
### Atomic operations and scenarios
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|||||||
@@ -1,15 +1,32 @@
|
|||||||
ActiveRecord
|
ActiveRecord
|
||||||
============
|
============
|
||||||
|
|
||||||
|
Scenarios
|
||||||
|
---------
|
||||||
|
|
||||||
|
All possible scenario formats supported by ActiveRecord:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function scenarios()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
// attributes array, all operations won't be wrapped with transaction
|
||||||
|
'scenario1' => array('attribute1', 'attribute2'),
|
||||||
|
|
||||||
|
// insert and update operations will be wrapped with transaction, delete won't be wrapped
|
||||||
|
'scenario2' => array(
|
||||||
|
'attributes' => array('attribute1', 'attribute2'),
|
||||||
|
'atomic' => array(self::OP_INSERT, self::OP_UPDATE),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Query
|
Query
|
||||||
-----
|
-----
|
||||||
|
|
||||||
### Basic Queries
|
### Basic Queries
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Relational Queries
|
### Relational Queries
|
||||||
|
|
||||||
### Scopes
|
### Scopes
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -590,18 +590,22 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the attribute names that are safe to be massively assigned in the current scenario.
|
* Returns the attribute names that are safe to be massively assigned in the current scenario.
|
||||||
* @return array safe attribute names
|
* @return string[] safe attribute names
|
||||||
*/
|
*/
|
||||||
public function safeAttributes()
|
public function safeAttributes()
|
||||||
{
|
{
|
||||||
$scenario = $this->getScenario();
|
$scenario = $this->getScenario();
|
||||||
$scenarios = $this->scenarios();
|
$scenarios = $this->scenarios();
|
||||||
|
if (!isset($scenarios[$scenario])) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
$attributes = array();
|
$attributes = array();
|
||||||
if (isset($scenarios[$scenario])) {
|
if (isset($scenarios[$scenario]['attributes']) && is_array($scenarios[$scenario]['attributes'])) {
|
||||||
foreach ($scenarios[$scenario] as $attribute) {
|
$scenarios[$scenario] = $scenarios[$scenario]['attributes'];
|
||||||
if ($attribute[0] !== '!') {
|
}
|
||||||
$attributes[] = $attribute;
|
foreach ($scenarios[$scenario] as $attribute) {
|
||||||
}
|
if ($attribute[0] !== '!') {
|
||||||
|
$attributes[] = $attribute;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $attributes;
|
return $attributes;
|
||||||
@@ -609,23 +613,26 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the attribute names that are subject to validation in the current scenario.
|
* Returns the attribute names that are subject to validation in the current scenario.
|
||||||
* @return array safe attribute names
|
* @return string[] safe attribute names
|
||||||
*/
|
*/
|
||||||
public function activeAttributes()
|
public function activeAttributes()
|
||||||
{
|
{
|
||||||
$scenario = $this->getScenario();
|
$scenario = $this->getScenario();
|
||||||
$scenarios = $this->scenarios();
|
$scenarios = $this->scenarios();
|
||||||
if (isset($scenarios[$scenario])) {
|
if (!isset($scenarios[$scenario])) {
|
||||||
$attributes = $scenarios[$this->getScenario()];
|
|
||||||
foreach ($attributes as $i => $attribute) {
|
|
||||||
if ($attribute[0] === '!') {
|
|
||||||
$attributes[$i] = substr($attribute, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $attributes;
|
|
||||||
} else {
|
|
||||||
return array();
|
return array();
|
||||||
}
|
}
|
||||||
|
if (isset($scenarios[$scenario]['attributes']) && is_array($scenarios[$scenario]['attributes'])) {
|
||||||
|
$attributes = $scenarios[$scenario]['attributes'];
|
||||||
|
} else {
|
||||||
|
$attributes = $scenarios[$scenario];
|
||||||
|
}
|
||||||
|
foreach ($attributes as $i => $attribute) {
|
||||||
|
if ($attribute[0] === '!') {
|
||||||
|
$attributes[$i] = substr($attribute, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -73,6 +73,22 @@ class ActiveRecord extends Model
|
|||||||
*/
|
*/
|
||||||
const EVENT_AFTER_DELETE = 'afterDelete';
|
const EVENT_AFTER_DELETE = 'afterDelete';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents insert ActiveRecord operation. This constant is used for specifying set of atomic operations
|
||||||
|
* for particular scenario in the [[scenarios()]] method.
|
||||||
|
*/
|
||||||
|
const OP_INSERT = 'insert';
|
||||||
|
/**
|
||||||
|
* Represents update ActiveRecord operation. This constant is used for specifying set of atomic operations
|
||||||
|
* for particular scenario in the [[scenarios()]] method.
|
||||||
|
*/
|
||||||
|
const OP_UPDATE = 'update';
|
||||||
|
/**
|
||||||
|
* Represents delete ActiveRecord operation. This constant is used for specifying set of atomic operations
|
||||||
|
* for particular scenario in the [[scenarios()]] method.
|
||||||
|
*/
|
||||||
|
const OP_DELETE = 'delete';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array attribute values indexed by attribute names
|
* @var array attribute values indexed by attribute names
|
||||||
*/
|
*/
|
||||||
@@ -664,10 +680,39 @@ class ActiveRecord extends Model
|
|||||||
* @param array $attributes list of attributes that need to be saved. Defaults to null,
|
* @param array $attributes list of attributes that need to be saved. Defaults to null,
|
||||||
* meaning all attributes that are loaded from DB will be saved.
|
* meaning all attributes that are loaded from DB will be saved.
|
||||||
* @return boolean whether the attributes are valid and the record is inserted successfully.
|
* @return boolean whether the attributes are valid and the record is inserted successfully.
|
||||||
|
* @throws \Exception in case insert failed.
|
||||||
*/
|
*/
|
||||||
public function insert($runValidation = true, $attributes = null)
|
public function insert($runValidation = true, $attributes = null)
|
||||||
{
|
{
|
||||||
if ($runValidation && !$this->validate($attributes) || !$this->beforeSave(true)) {
|
if ($runValidation && !$this->validate($attributes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$db = static::getDb();
|
||||||
|
$transaction = $this->isOperationAtomic(self::OP_INSERT) && $db->getTransaction() === null ? $db->beginTransaction() : null;
|
||||||
|
try {
|
||||||
|
$result = $this->insertInternal($attributes);
|
||||||
|
if ($transaction !== null) {
|
||||||
|
if ($result === false) {
|
||||||
|
$transaction->rollback();
|
||||||
|
} else {
|
||||||
|
$transaction->commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
if ($transaction !== null) {
|
||||||
|
$transaction->rollback();
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see ActiveRecord::insert()
|
||||||
|
*/
|
||||||
|
private function insertInternal($attributes = null)
|
||||||
|
{
|
||||||
|
if (!$this->beforeSave(true)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$values = $this->getDirtyAttributes($attributes);
|
$values = $this->getDirtyAttributes($attributes);
|
||||||
@@ -678,22 +723,23 @@ class ActiveRecord extends Model
|
|||||||
}
|
}
|
||||||
$db = static::getDb();
|
$db = static::getDb();
|
||||||
$command = $db->createCommand()->insert($this->tableName(), $values);
|
$command = $db->createCommand()->insert($this->tableName(), $values);
|
||||||
if ($command->execute()) {
|
if (!$command->execute()) {
|
||||||
$table = $this->getTableSchema();
|
return false;
|
||||||
if ($table->sequenceName !== null) {
|
}
|
||||||
foreach ($table->primaryKey as $name) {
|
$table = $this->getTableSchema();
|
||||||
if (!isset($this->_attributes[$name])) {
|
if ($table->sequenceName !== null) {
|
||||||
$this->_oldAttributes[$name] = $this->_attributes[$name] = $db->getLastInsertID($table->sequenceName);
|
foreach ($table->primaryKey as $name) {
|
||||||
break;
|
if (!isset($this->_attributes[$name])) {
|
||||||
}
|
$this->_oldAttributes[$name] = $this->_attributes[$name] = $db->getLastInsertID($table->sequenceName);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach ($values as $name => $value) {
|
|
||||||
$this->_oldAttributes[$name] = $value;
|
|
||||||
}
|
|
||||||
$this->afterSave(true);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
foreach ($values as $name => $value) {
|
||||||
|
$this->_oldAttributes[$name] = $value;
|
||||||
|
}
|
||||||
|
$this->afterSave(true);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -744,39 +790,67 @@ class ActiveRecord extends Model
|
|||||||
* or [[beforeSave()]] stops the updating process.
|
* or [[beforeSave()]] stops the updating process.
|
||||||
* @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
|
* @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
|
||||||
* being updated is outdated.
|
* being updated is outdated.
|
||||||
|
* @throws \Exception in case update failed.
|
||||||
*/
|
*/
|
||||||
public function update($runValidation = true, $attributes = null)
|
public function update($runValidation = true, $attributes = null)
|
||||||
{
|
{
|
||||||
if ($runValidation && !$this->validate($attributes) || !$this->beforeSave(false)) {
|
if ($runValidation && !$this->validate($attributes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$db = static::getDb();
|
||||||
|
$transaction = $this->isOperationAtomic(self::OP_UPDATE) && $db->getTransaction() === null ? $db->beginTransaction() : null;
|
||||||
|
try {
|
||||||
|
$result = $this->updateInternal($attributes);
|
||||||
|
if ($transaction !== null) {
|
||||||
|
if ($result === false) {
|
||||||
|
$transaction->rollback();
|
||||||
|
} else {
|
||||||
|
$transaction->commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
if ($transaction !== null) {
|
||||||
|
$transaction->rollback();
|
||||||
|
}
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see CActiveRecord::update()
|
||||||
|
* @throws StaleObjectException
|
||||||
|
*/
|
||||||
|
private function updateInternal($attributes = null)
|
||||||
|
{
|
||||||
|
if (!$this->beforeSave(false)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$values = $this->getDirtyAttributes($attributes);
|
$values = $this->getDirtyAttributes($attributes);
|
||||||
if (!empty($values)) {
|
if (empty($values)) {
|
||||||
$condition = $this->getOldPrimaryKey(true);
|
|
||||||
$lock = $this->optimisticLock();
|
|
||||||
if ($lock !== null) {
|
|
||||||
if (!isset($values[$lock])) {
|
|
||||||
$values[$lock] = $this->$lock + 1;
|
|
||||||
}
|
|
||||||
$condition[$lock] = $this->$lock;
|
|
||||||
}
|
|
||||||
// We do not check the return value of updateAll() because it's possible
|
|
||||||
// that the UPDATE statement doesn't change anything and thus returns 0.
|
|
||||||
$rows = $this->updateAll($values, $condition);
|
|
||||||
|
|
||||||
if ($lock !== null && !$rows) {
|
|
||||||
throw new StaleObjectException('The object being updated is outdated.');
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($values as $name => $value) {
|
|
||||||
$this->_oldAttributes[$name] = $this->_attributes[$name];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->afterSave(false);
|
|
||||||
return $rows;
|
|
||||||
} else {
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
$condition = $this->getOldPrimaryKey(true);
|
||||||
|
$lock = $this->optimisticLock();
|
||||||
|
if ($lock !== null) {
|
||||||
|
if (!isset($values[$lock])) {
|
||||||
|
$values[$lock] = $this->$lock + 1;
|
||||||
|
}
|
||||||
|
$condition[$lock] = $this->$lock;
|
||||||
|
}
|
||||||
|
// We do not check the return value of updateAll() because it's possible
|
||||||
|
// that the UPDATE statement doesn't change anything and thus returns 0.
|
||||||
|
$rows = $this->updateAll($values, $condition);
|
||||||
|
|
||||||
|
if ($lock !== null && !$rows) {
|
||||||
|
throw new StaleObjectException('The object being updated is outdated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($values as $name => $value) {
|
||||||
|
$this->_oldAttributes[$name] = $this->_attributes[$name];
|
||||||
|
}
|
||||||
|
$this->afterSave(false);
|
||||||
|
return $rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -826,27 +900,43 @@ class ActiveRecord extends Model
|
|||||||
* Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
|
* Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
|
||||||
* @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
|
* @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
|
||||||
* being deleted is outdated.
|
* being deleted is outdated.
|
||||||
|
* @throws \Exception in case delete failed.
|
||||||
*/
|
*/
|
||||||
public function delete()
|
public function delete()
|
||||||
{
|
{
|
||||||
if ($this->beforeDelete()) {
|
$db = static::getDb();
|
||||||
// we do not check the return value of deleteAll() because it's possible
|
$transaction = $this->isOperationAtomic(self::OP_DELETE) && $db->getTransaction() === null ? $db->beginTransaction() : null;
|
||||||
// the record is already deleted in the database and thus the method will return 0
|
try {
|
||||||
$condition = $this->getOldPrimaryKey(true);
|
$result = false;
|
||||||
$lock = $this->optimisticLock();
|
if ($this->beforeDelete()) {
|
||||||
if ($lock !== null) {
|
// we do not check the return value of deleteAll() because it's possible
|
||||||
$condition[$lock] = $this->$lock;
|
// the record is already deleted in the database and thus the method will return 0
|
||||||
|
$condition = $this->getOldPrimaryKey(true);
|
||||||
|
$lock = $this->optimisticLock();
|
||||||
|
if ($lock !== null) {
|
||||||
|
$condition[$lock] = $this->$lock;
|
||||||
|
}
|
||||||
|
$result = $this->deleteAll($condition);
|
||||||
|
if ($lock !== null && !$result) {
|
||||||
|
throw new StaleObjectException('The object being deleted is outdated.');
|
||||||
|
}
|
||||||
|
$this->_oldAttributes = null;
|
||||||
|
$this->afterDelete();
|
||||||
}
|
}
|
||||||
$rows = $this->deleteAll($condition);
|
if ($transaction !== null) {
|
||||||
if ($lock !== null && !$rows) {
|
if ($result === false) {
|
||||||
throw new StaleObjectException('The object being deleted is outdated.');
|
$transaction->rollback();
|
||||||
|
} else {
|
||||||
|
$transaction->commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$this->_oldAttributes = null;
|
} catch (\Exception $e) {
|
||||||
$this->afterDelete();
|
if ($transaction !== null) {
|
||||||
return $rows;
|
$transaction->rollback();
|
||||||
} else {
|
}
|
||||||
return false;
|
throw $e;
|
||||||
}
|
}
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1336,4 +1426,19 @@ class ActiveRecord extends Model
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $operation possible values are ActiveRecord::INSERT, ActiveRecord::UPDATE and ActiveRecord::DELETE.
|
||||||
|
* @return boolean whether given operation is atomic. Currently active scenario is taken into account.
|
||||||
|
*/
|
||||||
|
private function isOperationAtomic($operation)
|
||||||
|
{
|
||||||
|
$scenario = $this->getScenario();
|
||||||
|
$scenarios = $this->scenarios();
|
||||||
|
if (isset($scenarios[$scenario], $scenario[$scenario]['atomic']) && is_array($scenarios[$scenario]['atomic'])) {
|
||||||
|
return in_array($operation, $scenarios[$scenario]['atomic']);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user