Commit 79b79909 authored by romanb's avatar romanb

Refactored transactions. Fixed #464.

parent b309933a
...@@ -788,7 +788,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator ...@@ -788,7 +788,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator
$conn = $this->_table->getConnection(); $conn = $this->_table->getConnection();
} }
$conn->beginTransaction(); $conn->beginInternalTransaction();
$conn->transaction->addCollection($this); $conn->transaction->addCollection($this);
...@@ -817,7 +817,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator ...@@ -817,7 +817,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator
$conn = $this->_table->getConnection(); $conn = $this->_table->getConnection();
} }
$conn->beginTransaction(); $conn->beginInternalTransaction();
$conn->transaction->addCollection($this); $conn->transaction->addCollection($this);
foreach ($this as $key => $record) { foreach ($this as $key => $record) {
......
...@@ -1114,7 +1114,7 @@ abstract class Doctrine_Connection extends Doctrine_Configurable implements Coun ...@@ -1114,7 +1114,7 @@ abstract class Doctrine_Connection extends Doctrine_Configurable implements Coun
*/ */
public function flush() public function flush()
{ {
$this->beginTransaction(); $this->beginInternalTransaction();
$this->unitOfWork->saveAll(); $this->unitOfWork->saveAll();
$this->commit(); $this->commit();
} }
...@@ -1275,6 +1275,11 @@ abstract class Doctrine_Connection extends Doctrine_Configurable implements Coun ...@@ -1275,6 +1275,11 @@ abstract class Doctrine_Connection extends Doctrine_Configurable implements Coun
$this->transaction->beginTransaction($savepoint); $this->transaction->beginTransaction($savepoint);
} }
public function beginInternalTransaction($savepoint = null)
{
$this->transaction->beginInternalTransaction($savepoint);
}
/** /**
* commit * commit
* Commit the database changes done during a transaction that is in * Commit the database changes done during a transaction that is in
......
...@@ -149,7 +149,7 @@ class Doctrine_Connection_UnitOfWork extends Doctrine_Connection_Module ...@@ -149,7 +149,7 @@ class Doctrine_Connection_UnitOfWork extends Doctrine_Connection_Module
$record->state(Doctrine_Record::STATE_LOCKED); $record->state(Doctrine_Record::STATE_LOCKED);
$conn->beginTransaction(); $conn->beginInternalTransaction();
$saveLater = $this->saveRelated($record); $saveLater = $this->saveRelated($record);
$record->state($state); $record->state($state);
...@@ -260,7 +260,7 @@ class Doctrine_Connection_UnitOfWork extends Doctrine_Connection_Module ...@@ -260,7 +260,7 @@ class Doctrine_Connection_UnitOfWork extends Doctrine_Connection_Module
if ( ! $record->exists()) { if ( ! $record->exists()) {
return false; return false;
} }
$this->conn->beginTransaction(); $this->conn->beginInternalTransaction();
$event = new Doctrine_Event($record, Doctrine_Event::RECORD_DELETE); $event = new Doctrine_Event($record, Doctrine_Event::RECORD_DELETE);
......
...@@ -50,9 +50,21 @@ class Doctrine_Transaction extends Doctrine_Connection_Module ...@@ -50,9 +50,21 @@ class Doctrine_Transaction extends Doctrine_Connection_Module
const STATE_BUSY = 2; const STATE_BUSY = 2;
/** /**
* @var integer $transactionLevel the nesting level of transactions, used by transaction methods * @var integer $_nestingLevel The current nesting level of this transaction.
* A nesting level of 0 means there is currently no active
* transaction.
*/ */
protected $transactionLevel = 0; protected $_nestingLevel = 0;
/**
* @var integer $_internalNestingLevel The current internal nesting level of this transaction.
* "Internal" means transactions started by Doctrine itself.
* Therefore the internal nesting level is always
* lower or equal to the overall nesting level.
* A level of 0 means there is currently no active
* transaction that was initiated by Doctrine itself.
*/
protected $_internalNestingLevel = 0;
/** /**
* @var array $invalid an array containing all invalid records within this transaction * @var array $invalid an array containing all invalid records within this transaction
...@@ -90,14 +102,14 @@ class Doctrine_Transaction extends Doctrine_Connection_Module ...@@ -90,14 +102,14 @@ class Doctrine_Transaction extends Doctrine_Connection_Module
/** /**
* getState * getState
* returns the state of this connection * returns the state of this transaction module.
* *
* @see Doctrine_Connection_Transaction::STATE_* constants * @see Doctrine_Connection_Transaction::STATE_* constants
* @return integer the connection state * @return integer the connection state
*/ */
public function getState() public function getState()
{ {
switch ($this->transactionLevel) { switch ($this->_nestingLevel) {
case 0: case 0:
return Doctrine_Transaction::STATE_SLEEP; return Doctrine_Transaction::STATE_SLEEP;
break; break;
...@@ -132,7 +144,8 @@ class Doctrine_Transaction extends Doctrine_Connection_Module ...@@ -132,7 +144,8 @@ class Doctrine_Transaction extends Doctrine_Connection_Module
* *
* @return array An array of invalid records * @return array An array of invalid records
*/ */
public function getInvalid(){ public function getInvalid()
{
return $this->invalid; return $this->invalid;
} }
...@@ -144,20 +157,7 @@ class Doctrine_Transaction extends Doctrine_Connection_Module ...@@ -144,20 +157,7 @@ class Doctrine_Transaction extends Doctrine_Connection_Module
*/ */
public function getTransactionLevel() public function getTransactionLevel()
{ {
return $this->transactionLevel; return $this->_nestingLevel;
}
/**
* getTransactionLevel
* set the current transaction nesting level
*
* @return Doctrine_Transaction this object
*/
public function setTransactionLevel($level)
{
$this->transactionLevel = $level;
return $this;
} }
/** /**
...@@ -167,6 +167,9 @@ class Doctrine_Transaction extends Doctrine_Connection_Module ...@@ -167,6 +167,9 @@ class Doctrine_Transaction extends Doctrine_Connection_Module
* if trying to set a savepoint and there is no active transaction * if trying to set a savepoint and there is no active transaction
* a new transaction is being started * a new transaction is being started
* *
* This method should only be used by userland-code to initiate transactions.
* To initiate a transaction from inside Doctrine use {@link beginInternalTransaction()}.
*
* Listeners: onPreTransactionBegin, onTransactionBegin * Listeners: onPreTransactionBegin, onTransactionBegin
* *
* @param string $savepoint name of a savepoint to set * @param string $savepoint name of a savepoint to set
...@@ -192,7 +195,7 @@ class Doctrine_Transaction extends Doctrine_Connection_Module ...@@ -192,7 +195,7 @@ class Doctrine_Transaction extends Doctrine_Connection_Module
$listener->postSavepointCreate($event); $listener->postSavepointCreate($event);
} else { } else {
if ($this->transactionLevel == 0) { if ($this->_nestingLevel == 0) {
$event = new Doctrine_Event($this, Doctrine_Event::TX_BEGIN); $event = new Doctrine_Event($this, Doctrine_Event::TX_BEGIN);
$listener->preTransactionBegin($event); $listener->preTransactionBegin($event);
...@@ -200,7 +203,7 @@ class Doctrine_Transaction extends Doctrine_Connection_Module ...@@ -200,7 +203,7 @@ class Doctrine_Transaction extends Doctrine_Connection_Module
if ( ! $event->skipOperation) { if ( ! $event->skipOperation) {
try { try {
$this->conn->getDbh()->beginTransaction(); $this->conn->getDbh()->beginTransaction();
} catch(Exception $e) { } catch (Exception $e) {
throw new Doctrine_Transaction_Exception($e->getMessage()); throw new Doctrine_Transaction_Exception($e->getMessage());
} }
} }
...@@ -208,7 +211,7 @@ class Doctrine_Transaction extends Doctrine_Connection_Module ...@@ -208,7 +211,7 @@ class Doctrine_Transaction extends Doctrine_Connection_Module
} }
} }
$level = ++$this->transactionLevel; $level = ++$this->_nestingLevel;
return $level; return $level;
} }
...@@ -228,16 +231,16 @@ class Doctrine_Transaction extends Doctrine_Connection_Module ...@@ -228,16 +231,16 @@ class Doctrine_Transaction extends Doctrine_Connection_Module
*/ */
public function commit($savepoint = null) public function commit($savepoint = null)
{ {
$this->conn->connect(); if ($this->_nestingLevel == 0) {
throw new Doctrine_Transaction_Exception("Commit failed. There is no active transaction.");
if ($this->transactionLevel == 0) {
return false;
} }
$this->conn->connect();
$listener = $this->conn->getAttribute(Doctrine::ATTR_LISTENER); $listener = $this->conn->getAttribute(Doctrine::ATTR_LISTENER);
if ( ! is_null($savepoint)) { if ( ! is_null($savepoint)) {
$this->transactionLevel -= $this->removeSavePoints($savepoint); $this->_nestingLevel -= $this->removeSavePoints($savepoint);
$event = new Doctrine_Event($this, Doctrine_Event::SAVEPOINT_COMMIT); $event = new Doctrine_Event($this, Doctrine_Event::SAVEPOINT_COMMIT);
...@@ -249,13 +252,19 @@ class Doctrine_Transaction extends Doctrine_Connection_Module ...@@ -249,13 +252,19 @@ class Doctrine_Transaction extends Doctrine_Connection_Module
$listener->postSavepointCommit($event); $listener->postSavepointCommit($event);
} else { } else {
if ($this->transactionLevel == 1) {
if ($this->_nestingLevel == 1 || $this->_internalNestingLevel == 1) {
if ( ! empty($this->invalid)) { if ( ! empty($this->invalid)) {
if ($this->_internalNestingLevel == 1) {
// transaction was started by doctrine, so we are responsible
// for a rollback
$this->rollback(); $this->rollback();
$tmp = $this->invalid; $tmp = $this->invalid;
$this->invalid = array(); $this->invalid = array();
throw new Doctrine_Validator_Exception($tmp); throw new Doctrine_Validator_Exception($tmp);
} }
}
if ($this->_nestingLevel == 1) {
// take snapshots of all collections used within this transaction // take snapshots of all collections used within this transaction
foreach ($this->_collections as $coll) { foreach ($this->_collections as $coll) {
$coll->takeSnapshot(); $coll->takeSnapshot();
...@@ -269,10 +278,15 @@ class Doctrine_Transaction extends Doctrine_Connection_Module ...@@ -269,10 +278,15 @@ class Doctrine_Transaction extends Doctrine_Connection_Module
$this->conn->getDbh()->commit(); $this->conn->getDbh()->commit();
} }
$listener->postTransactionCommit($event); $listener->postTransactionCommit($event);
}
} }
$this->transactionLevel--; if ($this->_nestingLevel > 0) {
$this->_nestingLevel--;
}
if ($this->_internalNestingLevel > 0) {
$this->_internalNestingLevel--;
}
} }
return true; return true;
...@@ -300,17 +314,22 @@ class Doctrine_Transaction extends Doctrine_Connection_Module ...@@ -300,17 +314,22 @@ class Doctrine_Transaction extends Doctrine_Connection_Module
*/ */
public function rollback($savepoint = null) public function rollback($savepoint = null)
{ {
if ($this->_nestingLevel == 0) {
throw new Doctrine_Transaction_Exception("Rollback failed. There is no active transaction.");
}
$this->conn->connect(); $this->conn->connect();
$currentState = $this->getState();
if ($currentState != self::STATE_ACTIVE && $currentState != self::STATE_BUSY) { if ($this->_internalNestingLevel > 1 || $this->_nestingLevel > 1) {
$this->_internalNestingLevel--;
$this->_nestingLevel--;
return false; return false;
} }
$listener = $this->conn->getAttribute(Doctrine::ATTR_LISTENER); $listener = $this->conn->getAttribute(Doctrine::ATTR_LISTENER);
if ( ! is_null($savepoint)) { if ( ! is_null($savepoint)) {
$this->transactionLevel -= $this->removeSavePoints($savepoint); $this->_nestingLevel -= $this->removeSavePoints($savepoint);
$event = new Doctrine_Event($this, Doctrine_Event::SAVEPOINT_ROLLBACK); $event = new Doctrine_Event($this, Doctrine_Event::SAVEPOINT_ROLLBACK);
...@@ -327,7 +346,8 @@ class Doctrine_Transaction extends Doctrine_Connection_Module ...@@ -327,7 +346,8 @@ class Doctrine_Transaction extends Doctrine_Connection_Module
$listener->preTransactionRollback($event); $listener->preTransactionRollback($event);
if ( ! $event->skipOperation) { if ( ! $event->skipOperation) {
$this->transactionLevel = 0; $this->_nestingLevel = 0;
$this->_internalNestingLevel = 0;
try { try {
$this->conn->getDbh()->rollback(); $this->conn->getDbh()->rollback();
} catch (Exception $e) { } catch (Exception $e) {
...@@ -450,4 +470,17 @@ class Doctrine_Transaction extends Doctrine_Connection_Module ...@@ -450,4 +470,17 @@ class Doctrine_Transaction extends Doctrine_Connection_Module
{ {
throw new Doctrine_Transaction_Exception('Fetching transaction isolation level not supported by this driver.'); throw new Doctrine_Transaction_Exception('Fetching transaction isolation level not supported by this driver.');
} }
/**
* Initiates a transaction.
*
* This method must only be used by Doctrine itself to initiate transactions.
* Userland-code must use {@link beginTransaction()}.
*/
public function beginInternalTransaction($savepoint = null)
{
$this->_internalNestingLevel++;
return $this->beginTransaction($savepoint);
}
} }
...@@ -44,7 +44,7 @@ class Doctrine_Ticket_673_TestCase extends Doctrine_UnitTestCase ...@@ -44,7 +44,7 @@ class Doctrine_Ticket_673_TestCase extends Doctrine_UnitTestCase
->delete() ->delete()
->from('T673_Student s') ->from('T673_Student s')
->where('s.name = ? AND s.foo < ?', 'foo', 3); ->where('s.name = ? AND s.foo < ?', 'foo', 3);
var_dump($q->getSql());
$this->assertTrue(preg_match_all('/(s_name)/', $q->getSql(), $m) === 1); $this->assertTrue(preg_match_all('/(s_name)/', $q->getSql(), $m) === 1);
$this->assertTrue(preg_match_all('/(s_foo)/', $q->getSql(), $m) === 1); $this->assertTrue(preg_match_all('/(s_foo)/', $q->getSql(), $m) === 1);
......
...@@ -121,27 +121,21 @@ class Doctrine_Transaction_TestCase extends Doctrine_UnitTestCase ...@@ -121,27 +121,21 @@ class Doctrine_Transaction_TestCase extends Doctrine_UnitTestCase
public function testReleaseSavepointIsOnlyImplementedAtDriverLevel() public function testReleaseSavepointIsOnlyImplementedAtDriverLevel()
{ {
try { try {
$this->transaction->setTransactionLevel(1);
$this->transaction->commit('savepoint'); $this->transaction->commit('savepoint');
$this->fail(); $this->fail();
} catch(Doctrine_Transaction_Exception $e) { } catch (Doctrine_Transaction_Exception $e) {
$this->pass(); $this->pass();
} }
$this->transaction->setTransactionLevel(0);
} }
public function testRollbackSavepointIsOnlyImplementedAtDriverLevel() public function testRollbackSavepointIsOnlyImplementedAtDriverLevel()
{ {
try { try {
$this->transaction->setTransactionLevel(1);
$this->transaction->rollback('savepoint'); $this->transaction->rollback('savepoint');
$this->fail(); $this->fail();
} catch(Doctrine_Transaction_Exception $e) { } catch(Doctrine_Transaction_Exception $e) {
$this->pass(); $this->pass();
} }
$this->transaction->setTransactionLevel(0);
} }
public function testSetIsolationIsOnlyImplementedAtDriverLevel() public function testSetIsolationIsOnlyImplementedAtDriverLevel()
...@@ -174,14 +168,24 @@ class Doctrine_Transaction_TestCase extends Doctrine_UnitTestCase ...@@ -174,14 +168,24 @@ class Doctrine_Transaction_TestCase extends Doctrine_UnitTestCase
$this->assertEqual($this->transaction->getState(), Doctrine_Transaction::STATE_SLEEP); $this->assertEqual($this->transaction->getState(), Doctrine_Transaction::STATE_SLEEP);
} }
public function testCommittingNotActiveTransactionReturnsFalse() public function testCommittingWithNoActiveTransactionThrowsException()
{ {
$this->assertEqual($this->transaction->commit(), false); try {
$this->transaction->commit();
$this->fail();
} catch (Doctrine_Transaction_Exception $e) {
$this->pass();
}
} }
public function testExceptionIsThrownWhenUsingRollbackOnNotActiveTransaction() public function testExceptionIsThrownWhenUsingRollbackOnNotActiveTransaction()
{ {
$this->assertEqual($this->transaction->rollback(), false); try {
$this->transaction->rollback();
$this->fail();
} catch (Doctrine_Transaction_Exception $e) {
$this->pass();
}
} }
public function testBeginTransactionStartsNewTransaction() public function testBeginTransactionStartsNewTransaction()
......
...@@ -397,4 +397,32 @@ class Doctrine_Validator_TestCase extends Doctrine_UnitTestCase ...@@ -397,4 +397,32 @@ class Doctrine_Validator_TestCase extends Doctrine_UnitTestCase
$this->manager->setAttribute(Doctrine::ATTR_VALIDATE, Doctrine::VALIDATE_NONE); $this->manager->setAttribute(Doctrine::ATTR_VALIDATE, Doctrine::VALIDATE_NONE);
} }
public function testSaveInTransactionThrowsValidatorException()
{
$this->manager->setAttribute(Doctrine::ATTR_VALIDATE, Doctrine::VALIDATE_ALL);
try {
$this->conn->beginTransaction();
$client = new ValidatorTest_ClientModel();
$client->short_name = 'test';
$client->ValidatorTest_AddressModel[0]->state = 'az';
$client->save();
$this->fail();
$this->conn->commit();
} catch (Doctrine_Validator_Exception $dve) {
$s = $dve->getInvalidRecords();
$this->assertEqual(1, count($dve->getInvalidRecords()));
$stack = $client->ValidatorTest_AddressModel[0]->getErrorStack();
$this->assertTrue(in_array('notnull', $stack['address1']));
$this->assertTrue(in_array('notblank', $stack['address1']));
$this->assertTrue(in_array('notnull', $stack['address2']));
$this->assertTrue(in_array('notnull', $stack['city']));
$this->assertTrue(in_array('notblank', $stack['city']));
$this->assertTrue(in_array('usstate', $stack['state']));
$this->assertTrue(in_array('notnull', $stack['zip']));
$this->assertTrue(in_array('notblank', $stack['zip']));
}
$this->manager->setAttribute(Doctrine::ATTR_VALIDATE, Doctrine::VALIDATE_NONE);
}
} }
...@@ -153,7 +153,7 @@ $test->addTestCase($data_types); ...@@ -153,7 +153,7 @@ $test->addTestCase($data_types);
// Utility components // Utility components
$plugins = new GroupTest('Plugin tests: View, Validator, Hook','plugins'); $plugins = new GroupTest('Plugin tests: View, Validator, Hook','plugins');
//$utility->addTestCase(new Doctrine_PessimisticLocking_TestCase()); //$utility->addTestCase(new Doctrine_PessimisticLocking_TestCase());
$plugins->addTestCase(new Doctrine_Plugin_TestCase()); //$plugins->addTestCase(new Doctrine_Plugin_TestCase());
$plugins->addTestCase(new Doctrine_View_TestCase()); $plugins->addTestCase(new Doctrine_View_TestCase());
$plugins->addTestCase(new Doctrine_AuditLog_TestCase()); $plugins->addTestCase(new Doctrine_AuditLog_TestCase());
$plugins->addTestCase(new Doctrine_Validator_TestCase()); $plugins->addTestCase(new Doctrine_Validator_TestCase());
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment