Fix transactions do not get rollbacked in saveAssociated/saveMany

Refs #2849
This commit is contained in:
chinpei215 2014-08-02 10:12:33 +09:00
parent 3d77ce5d34
commit 799500ce6d
2 changed files with 253 additions and 137 deletions

View file

@ -2215,6 +2215,7 @@ class Model extends Object implements CakeEventListener {
* @return mixed If atomic: True on success, or false on failure. * @return mixed If atomic: True on success, or false on failure.
* Otherwise: array similar to the $data array passed, but values are set to true/false * Otherwise: array similar to the $data array passed, but values are set to true/false
* depending on whether each record saved successfully. * depending on whether each record saved successfully.
* @throws PDOException
* @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-savemany-array-data-null-array-options-array * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-savemany-array-data-null-array-options-array
*/ */
public function saveMany($data = null, $options = array()) { public function saveMany($data = null, $options = array()) {
@ -2245,47 +2246,58 @@ class Model extends Object implements CakeEventListener {
if ($options['atomic']) { if ($options['atomic']) {
$db = $this->getDataSource(); $db = $this->getDataSource();
$transactionBegun = $db->begin(); $transactionBegun = $db->begin();
} else {
$transactionBegun = false;
} }
$return = array(); try {
foreach ($data as $key => $record) { $return = array();
$validates = $this->create(null) !== null; foreach ($data as $key => $record) {
$saved = false; $validates = $this->create(null) !== null;
if ($validates) { $saved = false;
if ($options['deep']) { if ($validates) {
$saved = $this->saveAssociated($record, array_merge($options, array('atomic' => false))); if ($options['deep']) {
} else { $saved = $this->saveAssociated($record, array_merge($options, array('atomic' => false)));
$saved = $this->save($record, $options); } else {
$saved = $this->save($record, $options);
}
}
$validates = ($validates && ($saved === true || (is_array($saved) && !in_array(false, $saved, true))));
if (!$validates) {
$validationErrors[$key] = $this->validationErrors;
}
if (!$options['atomic']) {
$return[$key] = $validates;
} elseif (!$validates) {
break;
} }
} }
$validates = ($validates && ($saved === true || (is_array($saved) && !in_array(false, $saved, true)))); $this->validationErrors = $validationErrors;
if (!$validates) {
$validationErrors[$key] = $this->validationErrors;
}
if (!$options['atomic']) { if (!$options['atomic']) {
$return[$key] = $validates; return $return;
} elseif (!$validates) {
break;
} }
}
$this->validationErrors = $validationErrors; if ($validates) {
if ($transactionBegun) {
return $db->commit() !== false;
}
return true;
}
if (!$options['atomic']) {
return $return;
}
if ($validates) {
if ($transactionBegun) { if ($transactionBegun) {
return $db->commit() !== false; $db->rollback();
} }
return true; return false;
} catch (Exception $e) {
if ($transactionBegun) {
$db->rollback();
}
throw $e;
} }
$db->rollback();
return false;
} }
/** /**
@ -2337,6 +2349,7 @@ class Model extends Object implements CakeEventListener {
* @return mixed If atomic: True on success, or false on failure. * @return mixed If atomic: True on success, or false on failure.
* Otherwise: array similar to the $data array passed, but values are set to true/false * Otherwise: array similar to the $data array passed, but values are set to true/false
* depending on whether each record saved successfully. * depending on whether each record saved successfully.
* @throws PDOException
* @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveassociated-array-data-null-array-options-array * @link http://book.cakephp.org/2.0/en/models/saving-your-data.html#model-saveassociated-array-data-null-array-options-array
*/ */
public function saveAssociated($data = null, $options = array()) { public function saveAssociated($data = null, $options = array()) {
@ -2368,133 +2381,144 @@ class Model extends Object implements CakeEventListener {
if ($options['atomic']) { if ($options['atomic']) {
$db = $this->getDataSource(); $db = $this->getDataSource();
$transactionBegun = $db->begin(); $transactionBegun = $db->begin();
} else {
$transactionBegun = false;
} }
$associations = $this->getAssociated(); try {
$return = array(); $associations = $this->getAssociated();
$validates = true; $return = array();
foreach ($data as $association => $values) { $validates = true;
$isEmpty = empty($values) || (isset($values[$association]) && empty($values[$association])); foreach ($data as $association => $values) {
if ($isEmpty || !isset($associations[$association]) || $associations[$association] !== 'belongsTo') { $isEmpty = empty($values) || (isset($values[$association]) && empty($values[$association]));
continue; if ($isEmpty || !isset($associations[$association]) || $associations[$association] !== 'belongsTo') {
} continue;
$Model = $this->{$association};
$validates = $Model->create(null) !== null;
$saved = false;
if ($validates) {
if ($options['deep']) {
$saved = $Model->saveAssociated($values, array('atomic' => false) + $options);
} else {
$saved = $Model->save($values, array('atomic' => false) + $options);
} }
$validates = ($saved === true || (is_array($saved) && !in_array(false, $saved, true)));
}
if ($validates) { $Model = $this->{$association};
$key = $this->belongsTo[$association]['foreignKey'];
if (isset($data[$this->alias])) {
$data[$this->alias][$key] = $Model->id;
} else {
$data = array_merge(array($key => $Model->id), $data, array($key => $Model->id));
}
$options = $this->_addToWhiteList($key, $options);
} else {
$validationErrors[$association] = $Model->validationErrors;
}
$return[$association] = $validates; $validates = $Model->create(null) !== null;
} $saved = false;
if ($validates) {
if ($validates && !($this->create(null) !== null && $this->save($data, $options))) { if ($options['deep']) {
$validationErrors[$this->alias] = $this->validationErrors; $saved = $Model->saveAssociated($values, array('atomic' => false) + $options);
$validates = false;
}
$return[$this->alias] = $validates;
foreach ($data as $association => $values) {
if (!$validates) {
break;
}
$isEmpty = empty($values) || (isset($values[$association]) && empty($values[$association]));
if ($isEmpty || !isset($associations[$association])) {
continue;
}
$Model = $this->{$association};
$type = $associations[$association];
$key = $this->{$type}[$association]['foreignKey'];
switch ($type) {
case 'hasOne':
if (isset($values[$association])) {
$values[$association][$key] = $this->id;
} else { } else {
$values = array_merge(array($key => $this->id), $values, array($key => $this->id)); $saved = $Model->save($values, array('atomic' => false) + $options);
} }
$validates = ($saved === true || (is_array($saved) && !in_array(false, $saved, true)));
}
$validates = $Model->create(null) !== null; if ($validates) {
$saved = false; $key = $this->belongsTo[$association]['foreignKey'];
if (isset($data[$this->alias])) {
$data[$this->alias][$key] = $Model->id;
} else {
$data = array_merge(array($key => $Model->id), $data, array($key => $Model->id));
}
$options = $this->_addToWhiteList($key, $options);
} else {
$validationErrors[$association] = $Model->validationErrors;
}
$return[$association] = $validates;
}
if ($validates && !($this->create(null) !== null && $this->save($data, $options))) {
$validationErrors[$this->alias] = $this->validationErrors;
$validates = false;
}
$return[$this->alias] = $validates;
foreach ($data as $association => $values) {
if (!$validates) {
break;
}
$isEmpty = empty($values) || (isset($values[$association]) && empty($values[$association]));
if ($isEmpty || !isset($associations[$association])) {
continue;
}
$Model = $this->{$association};
$type = $associations[$association];
$key = $this->{$type}[$association]['foreignKey'];
switch ($type) {
case 'hasOne':
if (isset($values[$association])) {
$values[$association][$key] = $this->id;
} else {
$values = array_merge(array($key => $this->id), $values, array($key => $this->id));
}
$validates = $Model->create(null) !== null;
$saved = false;
if ($validates) {
$options = $Model->_addToWhiteList($key, $options);
if ($options['deep']) {
$saved = $Model->saveAssociated($values, array('atomic' => false) + $options);
} else {
$saved = $Model->save($values, $options);
}
}
$validates = ($validates && ($saved === true || (is_array($saved) && !in_array(false, $saved, true))));
if (!$validates) {
$validationErrors[$association] = $Model->validationErrors;
}
$return[$association] = $validates;
break;
case 'hasMany':
foreach ($values as $i => $value) {
if (isset($values[$i][$association])) {
$values[$i][$association][$key] = $this->id;
} else {
$values[$i] = array_merge(array($key => $this->id), $value, array($key => $this->id));
}
}
if ($validates) {
$options = $Model->_addToWhiteList($key, $options); $options = $Model->_addToWhiteList($key, $options);
if ($options['deep']) { $_return = $Model->saveMany($values, array('atomic' => false) + $options);
$saved = $Model->saveAssociated($values, array('atomic' => false) + $options); if (in_array(false, $_return, true)) {
} else { $validationErrors[$association] = $Model->validationErrors;
$saved = $Model->save($values, $options); $validates = false;
} }
}
$validates = ($validates && ($saved === true || (is_array($saved) && !in_array(false, $saved, true)))); $return[$association] = $_return;
if (!$validates) { break;
$validationErrors[$association] = $Model->validationErrors; }
}
$return[$association] = $validates;
break;
case 'hasMany':
foreach ($values as $i => $value) {
if (isset($values[$i][$association])) {
$values[$i][$association][$key] = $this->id;
} else {
$values[$i] = array_merge(array($key => $this->id), $value, array($key => $this->id));
}
}
$options = $Model->_addToWhiteList($key, $options);
$_return = $Model->saveMany($values, array('atomic' => false) + $options);
if (in_array(false, $_return, true)) {
$validationErrors[$association] = $Model->validationErrors;
$validates = false;
}
$return[$association] = $_return;
break;
} }
} $this->validationErrors = $validationErrors;
$this->validationErrors = $validationErrors;
if (isset($validationErrors[$this->alias])) { if (isset($validationErrors[$this->alias])) {
$this->validationErrors = $validationErrors[$this->alias]; $this->validationErrors = $validationErrors[$this->alias];
unset($validationErrors[$this->alias]); unset($validationErrors[$this->alias]);
$this->validationErrors = array_merge($this->validationErrors, $validationErrors); $this->validationErrors = array_merge($this->validationErrors, $validationErrors);
} }
if (!$options['atomic']) {
return $return;
}
if ($validates) {
if ($transactionBegun) {
return $db->commit() !== false;
}
return true;
}
if (!$options['atomic']) {
return $return;
}
if ($validates) {
if ($transactionBegun) { if ($transactionBegun) {
return $db->commit() !== false; $db->rollback();
} }
return false;
return true; } catch (Exception $e) {
if ($transactionBegun) {
$db->rollback();
}
throw $e;
} }
$db->rollback();
return false;
} }
/** /**

View file

@ -4143,6 +4143,25 @@ class ModelWriteTest extends BaseModelTest {
); );
$Post->saveAll($data, array('atomic' => true, 'validate' => true)); $Post->saveAll($data, array('atomic' => true, 'validate' => true));
// If exception thrown, rollback() should be called too.
$db = $this->_getMockDboSource(array('begin', 'commit', 'rollback'));
$db->expects($this->once())->method('begin')->will($this->returnValue(true));
$db->expects($this->never())->method('commit');
$db->expects($this->once())->method('rollback');
$Post->setDataSourceObject($db);
$data = array(
array('author_id' => 1, 'title' => 'New Fourth Post'),
array('author_id' => 1, 'title' => 'New Fifth Post', 'body' => $db->expression('PDO_EXCEPTION()'))
);
try {
$Post->saveAll($data, array('atomic' => true, 'validate' => true));
$this->fail('No exception thrown');
} catch (PDOException $e) {
}
// Otherwise, commit() should be called. // Otherwise, commit() should be called.
$db = $this->_getMockDboSource(array('begin', 'commit', 'rollback')); $db = $this->_getMockDboSource(array('begin', 'commit', 'rollback'));
$db->expects($this->once())->method('begin')->will($this->returnValue(true)); $db->expects($this->once())->method('begin')->will($this->returnValue(true));
@ -4193,6 +4212,33 @@ class ModelWriteTest extends BaseModelTest {
); );
$Post->saveAll($data, array('validate' => true)); $Post->saveAll($data, array('validate' => true));
// If exception thrown, rollback() should be called too.
$db = $this->_getMockDboSource(array('begin', 'commit', 'rollback'));
$db->expects($this->once())->method('begin')->will($this->returnValue(true));
$db->expects($this->never())->method('commit');
$db->expects($this->once())->method('rollback');
$Post->setDataSourceObject($db);
$Post->Author->setDataSourceObject($db);
$data = array(
'Post' => array(
'title' => 'New post',
'body' => $db->expression('PDO_EXCEPTION()'),
'published' => 'Y'
),
'Author' => array(
'user' => 'New user',
'password' => "sekret"
)
);
try {
$Post->saveAll($data, array('validate' => true));
$this->fail('No exception thrown');
} catch (PDOException $e) {
}
// Otherwise, commit() should be called. // Otherwise, commit() should be called.
$db = $this->_getMockDboSource(array('begin', 'commit', 'rollback')); $db = $this->_getMockDboSource(array('begin', 'commit', 'rollback'));
$db->expects($this->once())->method('begin')->will($this->returnValue(true)); $db->expects($this->once())->method('begin')->will($this->returnValue(true));
@ -5605,6 +5651,25 @@ class ModelWriteTest extends BaseModelTest {
); );
$Post->saveMany($data, array('validate' => true)); $Post->saveMany($data, array('validate' => true));
// If exception thrown, rollback() should be called too.
$db = $this->_getMockDboSource(array('begin', 'commit', 'rollback'));
$db->expects($this->once())->method('begin')->will($this->returnValue(true));
$db->expects($this->never())->method('commit');
$db->expects($this->once())->method('rollback');
$Post->setDataSourceObject($db);
$data = array(
array('author_id' => 1, 'title' => 'New Fourth Post'),
array('author_id' => 1, 'title' => 'New Fifth Post', 'body' => $db->expression('PDO_EXCEPTION()'))
);
try {
$Post->saveMany($data, array('validate' => true));
$this->fail('No exception thrown');
} catch (PDOException $e) {
}
// Otherwise, commit() should be called. // Otherwise, commit() should be called.
$db = $this->_getMockDboSource(array('begin', 'commit', 'rollback')); $db = $this->_getMockDboSource(array('begin', 'commit', 'rollback'));
$db->expects($this->once())->method('begin')->will($this->returnValue(true)); $db->expects($this->once())->method('begin')->will($this->returnValue(true));
@ -5655,6 +5720,33 @@ class ModelWriteTest extends BaseModelTest {
); );
$Post->saveAssociated($data, array('validate' => true, 'atomic' => true)); $Post->saveAssociated($data, array('validate' => true, 'atomic' => true));
// If exception thrown, commit() should be called.
$db = $this->_getMockDboSource(array('begin', 'commit', 'rollback'));
$db->expects($this->once())->method('begin')->will($this->returnValue(true));
$db->expects($this->never())->method('commit');
$db->expects($this->once())->method('rollback');
$Post->setDataSourceObject($db);
$Post->Author->setDataSourceObject($db);
$data = array(
'Post' => array(
'title' => 'New post',
'body' => $db->expression('PDO_EXCEPTION()'),
'published' => 'Y'
),
'Author' => array(
'user' => 'New user',
'password' => "sekret"
)
);
try {
$Post->saveAssociated($data, array('validate' => true, 'atomic' => true));
$this->fail('No exception thrown');
} catch (PDOException $e) {
}
// Otherwise, commit() should be called. // Otherwise, commit() should be called.
$db = $this->_getMockDboSource(array('begin', 'commit', 'rollback')); $db = $this->_getMockDboSource(array('begin', 'commit', 'rollback'));
$db->expects($this->once())->method('begin')->will($this->returnValue(true)); $db->expects($this->once())->method('begin')->will($this->returnValue(true));