Skip to content

Commit

Permalink
add 'atomic' option to "save()" API
Browse files Browse the repository at this point in the history
This commit adds a transaction context to 'save()' API in order to rollback
possible modifications done in some 'Model.beforeSave' listener callback.
This will allow cakephp 2.x to behave like 3.0 .
It uses try/catch to better handle transaction.
Previous save() API is renamed to protected _doSave() method.
A new save() method is created for transaction handling.
'atomic' option is disabled for internal 'save()' call.
  • Loading branch information
Haititi committed Jun 3, 2014
1 parent a135378 commit 31615ce
Show file tree
Hide file tree
Showing 2 changed files with 306 additions and 11 deletions.
65 changes: 54 additions & 11 deletions lib/Cake/Model/Model.php
Expand Up @@ -1671,13 +1671,15 @@ public function saveField($name, $value, $validate = false) {

/**
* Saves model data (based on white-list, if supplied) to the database. By
* default, validation occurs before save.
* default, validation occurs before save. Passthrough method to _doSave() with
* transaction handling.
*
* @param array $data Data to save.
* @param boolean|array $validate Either a boolean, or an array.
* If a boolean, indicates whether or not to validate before saving.
* If an array, can have following keys:
*
* - atomic: If true (default), will attempt to save the record in a single transaction.
* - validate: Set to true/false to enable or disable validation.
* - fieldList: An array of fields you want to allow for saving.
* - callbacks: Set to false to disable callbacks. Using 'before' or 'after'
Expand All @@ -1691,16 +1693,59 @@ public function saveField($name, $value, $validate = false) {
public function save($data = null, $validate = true, $fieldList = array()) {
$defaults = array(
'validate' => true, 'fieldList' => array(),
'callbacks' => true, 'counterCache' => true
'callbacks' => true, 'counterCache' => true,
'atomic' => true
);
$_whitelist = $this->whitelist;
$fields = array();

if (!is_array($validate)) {
$options = compact('validate', 'fieldList') + $defaults;
} else {
$options = $validate + $defaults;
}

if (!$options['atomic']) {
return $this->_doSave($data, $options);
}

$db = $this->getDataSource();
$transactionBegun = $db->begin();
try {
$success = $this->_doSave($data, $options);
if ($transactionBegun) {
if ($success) {
$db->commit();
} else {
$db->rollback();
}
}
return $success;
} catch (Exception $e) {
if ($transactionBegun) {
$db->rollback();
}
throw $e;
}
}

/**
* Saves model data (based on white-list, if supplied) to the database. By
* default, validation occurs before save.
*
* @param array $data Data to save.
* @param array $options can have following keys:
*
* - validate: Set to true/false to enable or disable validation.
* - fieldList: An array of fields you want to allow for saving.
* - callbacks: Set to false to disable callbacks. Using 'before' or 'after'
* will enable only those callbacks.
* - `counterCache`: Boolean to control updating of counter caches (if any)
*
* @return mixed On success Model::$data if its not empty or true, false on failure
* @link http://book.cakephp.org/2.0/en/models/saving-your-data.html
*/
protected function _doSave($data = null, $options = array()) {
$_whitelist = $this->whitelist;
$fields = array();

if (!empty($options['fieldList'])) {
if (!empty($options['fieldList'][$this->alias]) && is_array($options['fieldList'][$this->alias])) {
Expand Down Expand Up @@ -1779,8 +1824,6 @@ public function save($data = null, $validate = true, $fieldList = array()) {
}
}

$db = $this->getDataSource();

if (empty($this->data[$this->alias][$this->primaryKey])) {
unset($this->data[$this->alias][$this->primaryKey]);
}
Expand Down Expand Up @@ -1997,7 +2040,7 @@ protected function _saveMulti($joined, $id, $db) {
$Model->create();
}

$Model->save($data);
$Model->save($data, array('atomic' => false));
}
}

Expand Down Expand Up @@ -2241,9 +2284,9 @@ public function saveMany($data = null, $options = array()) {
$saved = false;
if ($validates) {
if ($options['deep']) {
$saved = $this->saveAssociated($record, array_merge($options, array('atomic' => false)));
$saved = $this->saveAssociated($record, array('atomic' => false) + $options);
} else {
$saved = $this->save($record, $options);
$saved = $this->save($record, array('atomic' => false) + $options);
}
}

Expand Down Expand Up @@ -2395,7 +2438,7 @@ public function saveAssociated($data = null, $options = array()) {
$return[$association] = $validates;
}

if ($validates && !($this->create(null) !== null && $this->save($data, $options))) {
if ($validates && !($this->create(null) !== null && $this->save($data, array('atomic' => false) + $options))) {
$validationErrors[$this->alias] = $this->validationErrors;
$validates = false;
}
Expand Down Expand Up @@ -2431,7 +2474,7 @@ public function saveAssociated($data = null, $options = array()) {
if ($options['deep']) {
$saved = $Model->saveAssociated($values, array('atomic' => false) + $options);
} else {
$saved = $Model->save($values, $options);
$saved = $Model->save($values, array('atomic' => false) + $options);
}
}

Expand Down
252 changes: 252 additions & 0 deletions lib/Cake/Test/Case/Model/ModelWriteTest.php
Expand Up @@ -702,6 +702,258 @@ public function testBeforeSaveSaveAbortion() {
$this->assertFalse($result);
}

/**
* testSaveAtomic method
*
* @return void
*/
public function testSaveAtomic() {
$this->loadFixtures('Article');
$TestModel = new Article();

// Create record with 'atomic' = false

$data = array(
'Article' => array(
'user_id' => '1',
'title' => 'Fourth Article',
'body' => 'Fourth Article Body',
'published' => 'Y'
)
);
$TestModel->create();
$result = $TestModel->save($data, array('atomic' => false));
$this->assertTrue((bool)$result);

// Check record we created

$TestModel->recursive = -1;
$result = $TestModel->read(array('id', 'user_id', 'title', 'body', 'published'), 4);
$expected = array(
'Article' => array(
'id' => '4',
'user_id' => '1',
'title' => 'Fourth Article',
'body' => 'Fourth Article Body',
'published' => 'Y'
)
);
$this->assertEquals($expected, $result);

// Create record with 'atomic' = true

$data = array(
'Article' => array(
'user_id' => '4',
'title' => 'Fifth Article',
'body' => 'Fifth Article Body',
'published' => 'Y'
)
);
$TestModel->create();
$result = $TestModel->save($data, array('atomic' => true));
$this->assertTrue((bool)$result);

// Check record we created

$TestModel->recursive = -1;
$result = $TestModel->read(array('id', 'user_id', 'title', 'body', 'published'), 5);
$expected = array(
'Article' => array(
'id' => '5',
'user_id' => '4',
'title' => 'Fifth Article',
'body' => 'Fifth Article Body',
'published' => 'Y'
)
);
$this->assertEquals($expected, $result);
}

/**
* test save with transaction and ensure there is no missing rollback.
*
* @return void
*/
public function testSaveTransactionNoRollback() {
$this->loadFixtures('Post', 'Article');

$db = $this->getMock('DboSource', array('begin', 'connect', 'rollback', 'describe'));

$db->expects($this->once())
->method('describe')
->will($this->returnValue(array()));
$db->expects($this->once())
->method('begin')
->will($this->returnValue(true));
$db->expects($this->once())
->method('rollback');

$Post = new TestPost();
$Post->setDataSourceObject($db);

$callback = array($this, 'callbackForTestSaveTransaction');
$Post->getEventManager()->attach($callback, 'Model.beforeSave');

$data = array(
'Post' => array(
'author_id' => 1,
'title' => 'New Fourth Post'
)
);
$Post->save($data, array('atomic' => true));
}

/**
* test callback used in testSaveTransaction method
*
* @return boolean false to stop event propagation
*/
public function callbackForTestSaveTransaction($event) {
$TestModel = new Article();

// Create record. Do not use same model as in testSaveTransaction
// to avoid infinite loop.

$data = array(
'Article' => array(
'user_id' => '1',
'title' => 'Fourth Article',
'body' => 'Fourth Article Body',
'published' => 'Y'
)
);
$TestModel->create();
$result = $TestModel->save($data);
$this->assertTrue((bool)$result);

// force transaction to be rolled back in Post model
$event->stopPropagation();
return false;
}

/**
* testSaveTransaction method
*
* @return void
*/
public function testSaveTransaction() {
$this->loadFixtures('Post', 'Article');
$PostModel = new Post();

// Check if Database supports transactions

$PostModel->validate = array('title' => 'notEmpty');
$data = array(
array('author_id' => 1, 'title' => 'New Fourth Post'),
array('author_id' => 1, 'title' => 'New Fifth Post'),
array('author_id' => 1, 'title' => '')
);
$this->assertFalse($PostModel->saveAll($data));

$result = $PostModel->find('all', array('recursive' => -1));
$expectedPosts = array(
array(
'Post' => array(
'id' => '1',
'author_id' => 1,
'title' => 'First Post',
'body' => 'First Post Body',
'published' => 'Y',
'created' => '2007-03-18 10:39:23',
'updated' => '2007-03-18 10:41:31'
)
),
array(
'Post' => array(
'id' => '2',
'author_id' => 3,
'title' => 'Second Post',
'body' => 'Second Post Body',
'published' => 'Y',
'created' => '2007-03-18 10:41:23',
'updated' => '2007-03-18 10:43:31'
)
),
array(
'Post' => array(
'id' => '3',
'author_id' => 1,
'title' => 'Third Post',
'body' => 'Third Post Body',
'published' => 'Y',
'created' => '2007-03-18 10:43:23',
'updated' => '2007-03-18 10:45:31'
)
)
);

$this->skipIf(count($result) !== 3, 'Database does not support transactions.');

$this->assertEquals($expectedPosts, $result);

// Database supports transactions --> continue tests

$data = array(
'Post' => array(
'author_id' => 1,
'title' => 'New Fourth Post'
)
);

$callback = array($this, 'callbackForTestSaveTransaction');
$PostModel->getEventManager()->attach($callback, 'Model.beforeSave');

$PostModel->create();
$result = $PostModel->save($data, array('atomic' => true));
$this->assertFalse($result);

$result = $PostModel->find('all', array('recursive' => -1));
$this->assertEquals($expectedPosts, $result);

// Check record we created in callbackForTestSaveTransaction method.
// record should not exist due to rollback

$ArticleModel = new Article();
$result = $ArticleModel->find('all', array('recursive' => -1));
$expectedArticles = array(
array(
'Article' => array(
'user_id' => '1',
'title' => 'First Article',
'body' => 'First Article Body',
'published' => 'Y',
'created' => '2007-03-18 10:39:23',
'updated' => '2007-03-18 10:41:31',
'id' => '1'
)
),
array(
'Article' => array(
'user_id' => '3',
'title' => 'Second Article',
'body' => 'Second Article Body',
'published' => 'Y',
'created' => '2007-03-18 10:41:23',
'updated' => '2007-03-18 10:43:31',
'id' => '2'
)
),
array(
'Article' => array(
'user_id' => '1',
'title' => 'Third Article',
'body' => 'Third Article Body',
'published' => 'Y',
'created' => '2007-03-18 10:43:23',
'updated' => '2007-03-18 10:45:31',
'id' => '3'
)
)
);
$this->assertEquals($expectedArticles, $result);
}

/**
* testSaveField method
*
Expand Down

0 comments on commit 31615ce

Please sign in to comment.