diff --git a/lib/Cake/Model/Model.php b/lib/Cake/Model/Model.php index f04f1c27d3b..f5e8ed1d433 100644 --- a/lib/Cake/Model/Model.php +++ b/lib/Cake/Model/Model.php @@ -2243,11 +2243,10 @@ public function saveMany($data = null, $options = array()) { $options['validate'] = false; } + $transactionBegun = false; if ($options['atomic']) { $db = $this->getDataSource(); $transactionBegun = $db->begin(); - } else { - $transactionBegun = false; } try { @@ -2378,11 +2377,10 @@ public function saveAssociated($data = null, $options = array()) { $options['validate'] = false; } + $transactionBegun = false; if ($options['atomic']) { $db = $this->getDataSource(); $transactionBegun = $db->begin(); - } else { - $transactionBegun = false; } try { diff --git a/lib/Cake/Test/Case/Model/ModelWriteTest.php b/lib/Cake/Test/Case/Model/ModelWriteTest.php index 2c7be723da4..6a523e5a1a9 100644 --- a/lib/Cake/Test/Case/Model/ModelWriteTest.php +++ b/lib/Cake/Test/Case/Model/ModelWriteTest.php @@ -36,6 +36,8 @@ class TestAuthor extends Author { protected $_dataSourceObject; + public $dataForAfterSave; + /** * Helper method to set a datasource object * @@ -74,6 +76,8 @@ class TestPost extends Post { protected $_dataSourceObject; + public $dataForAfterSave; + /** * Helper method to set a datasource object * @@ -7499,4 +7503,169 @@ protected function _getMockDboSource($methods = array()) { return $db; } + +/** + * Test that transactions behave correctly on nested saveMany calls. + * + * @return void + */ + public function testTransactionOnNestedSaveMany() { + $this->loadFixtures('Post'); + $Post = new TestPost(); + $Post->getEventManager()->attach(array($this, 'nestedSaveMany'), 'Model.afterSave'); + + // begin -> [ begin -> commit ] -> commit + $db = $this->_getMockDboSource(array('begin', 'commit', 'rollback')); + $db->expects($this->exactly(2))->method('begin')->will($this->returnValue(true)); + $db->expects($this->exactly(2))->method('commit'); + $db->expects($this->never())->method('rollback'); + $Post->setDataSourceObject($db); + + $data = array( + array('author_id' => 1, 'title' => 'Outer Post'), + ); + $Post->dataForAfterSave = array( + array('author_id' => 1, 'title' => 'Inner Post'), + ); + $this->assertTrue($Post->saveMany($data)); + + // begin -> [ begin(false) ] -> commit + $db = $this->_getMockDboSource(array('begin', 'commit', 'rollback')); + $db->expects($this->at(0))->method('begin')->will($this->returnValue(true)); + $db->expects($this->at(1))->method('begin')->will($this->returnValue(false)); + $db->expects($this->once())->method('commit'); + $db->expects($this->never())->method('rollback'); + $Post->setDataSourceObject($db); + + $data = array( + array('author_id' => 1, 'title' => 'Outer Post'), + ); + $Post->dataForAfterSave = array( + array('author_id' => 1, 'title' => 'Inner Post'), + ); + $this->assertTrue($Post->saveMany($data)); + + // begin -> [ begin -> rollback ] -> rollback + $db = $this->_getMockDboSource(array('begin', 'commit', 'rollback')); + $db->expects($this->exactly(2))->method('begin')->will($this->returnValue(true)); + $db->expects($this->never())->method('commit'); + $db->expects($this->exactly(2))->method('rollback'); + $Post->setDataSourceObject($db); + $data = array( + array('author_id' => 1, 'title' => 'Outer Post'), + ); + $Post->dataForAfterSave = array( + array('author_id' => 1, 'title' => 'Inner Post', 'body' => $db->expression('PDO_EXCEPTION()')), + ); + + try { + $Post->saveMany($data); + $this->fail('No exception thrown'); + } catch(Exception $e) { + } + } + +/** + * Test that transaction behaves correctly on nested saveAssociated calls. + * + * @return void + */ + public function testTransactionOnNestedSaveAssociated() { + $this->loadFixtures('Author', 'Post'); + + $Author = new TestAuthor(); + $Author->getEventManager()->attach(array($this, 'nestedSaveAssociated'), 'Model.afterSave'); + + // begin -> [ begin -> commit ] -> commit + $db = $this->_getMockDboSource(array('begin', 'commit', 'rollback')); + $db->expects($this->exactly(2))->method('begin')->will($this->returnValue(true)); + $db->expects($this->exactly(2))->method('commit'); + $db->expects($this->never())->method('rollback'); + $Author->setDataSourceObject($db); + $Author->Post->setDataSourceObject($db); + + $data = array( + 'Author' => array('user' => 'outer'), + 'Post' => array( + array('title' => 'Outer Post'), + ) + ); + $Author->dataForAfterSave = array( + 'Author' => array('user' => 'inner'), + 'Post' => array( + array('title' => 'Inner Post'), + ) + ); + $this->assertTrue($Author->saveAssociated($data)); + + // begin -> [ begin(false) ] -> commit + $db = $this->_getMockDboSource(array('begin', 'commit', 'rollback')); + $db->expects($this->at(0))->method('begin')->will($this->returnValue(true)); + $db->expects($this->at(1))->method('begin')->will($this->returnValue(false)); + $db->expects($this->once())->method('commit'); + $db->expects($this->never())->method('rollback'); + $Author->setDataSourceObject($db); + $Author->Post->setDataSourceObject($db); + $data = array( + 'Author' => array('user' => 'outer'), + 'Post' => array( + array('title' => 'Outer Post'), + ) + ); + $Author->dataForAfterSave = array( + 'Author' => array('user' => 'inner'), + 'Post' => array( + array('title' => 'Inner Post'), + ) + ); + $this->assertTrue($Author->saveAssociated($data)); + + // begin -> [ begin -> rollback ] -> rollback + $db = $this->_getMockDboSource(array('begin', 'commit', 'rollback')); + $db->expects($this->exactly(2))->method('begin')->will($this->returnValue(true)); + $db->expects($this->never())->method('commit'); + $db->expects($this->exactly(2))->method('rollback'); + $Author->setDataSourceObject($db); + $Author->Post->setDataSourceObject($db); + $data = array( + 'Author' => array('user' => 'outer'), + 'Post' => array( + array('title' => 'Outer Post'), + ) + ); + $Author->dataForAfterSave = array( + 'Author' => array('user' => 'inner', 'password' => $db->expression('PDO_EXCEPTION()')), + 'Post' => array( + array('title' => 'Inner Post'), + ) + ); + + try { + $Author->saveAssociated($data); + $this->fail('No exception thrown'); + } catch(Exception $e) { + } + } + +/** + * A callback for testing nested saveMany. + * + * @param CakeEvent $event containing the Model + * @return void + */ + public function nestedSaveMany($event) { + $Model = $event->subject; + $Model->saveMany($Model->dataForAfterSave, array('callbacks' => false)); + } + +/** + * A callback for testing nested saveAssociated. + * + * @param CakeEvent $event containing the Model + * @return void + */ + public function nestedSaveAssociated($event) { + $Model = $event->subject; + $Model->saveAssociated($Model->dataForAfterSave, array('callbacks' => false)); + } }