From 5dbd40563cdd559edf609e353b0f22ed6b6af52c Mon Sep 17 00:00:00 2001 From: "Roman S. Borschel" Date: Thu, 13 May 2010 13:19:59 +0200 Subject: [PATCH] Added control abstractions for transaction demarcation. --- lib/Doctrine/DBAL/Connection.php | 24 ++++++++++++++- lib/Doctrine/ORM/EntityManager.php | 29 ++++++++++++++++++- lib/Doctrine/ORM/UnitOfWork.php | 2 ++ .../Tests/DBAL/Functional/ConnectionTest.php | 10 +++++++ .../ORM/Functional/BasicFunctionalTest.php | 8 ++--- 5 files changed, 67 insertions(+), 6 deletions(-) diff --git a/lib/Doctrine/DBAL/Connection.php b/lib/Doctrine/DBAL/Connection.php index 3ad6e6e0b..a2b277ebd 100644 --- a/lib/Doctrine/DBAL/Connection.php +++ b/lib/Doctrine/DBAL/Connection.php @@ -19,7 +19,7 @@ namespace Doctrine\DBAL; -use PDO, Closure, +use PDO, Closure, Exception, Doctrine\DBAL\Types\Type, Doctrine\DBAL\Driver\Connection as DriverConnection, Doctrine\Common\EventManager, @@ -705,6 +705,28 @@ class Connection implements DriverConnection return $this->_conn->lastInsertId($seqName); } + /** + * Executes a function in a transaction. + * + * The function gets passed this Connection instance as an (optional) parameter. + * + * If an exception occurs during execution of the function or transaction commit, + * the transaction is rolled back and the exception re-thrown. + * + * @param Closure $func The function to execute transactionally. + */ + public function transactional(Closure $func) + { + $this->beginTransaction(); + try { + $func($this); + $this->commit(); + } catch (Exception $e) { + $this->rollback(); + throw $e; + } + } + /** * Starts a transaction by suspending auto-commit mode. * diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index c906729dd..e5ce854ae 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -19,7 +19,8 @@ namespace Doctrine\ORM; -use Doctrine\Common\EventManager, +use Closure, Exception, + Doctrine\Common\EventManager, Doctrine\DBAL\Connection, Doctrine\ORM\Mapping\ClassMetadata, Doctrine\ORM\Mapping\ClassMetadataFactory, @@ -176,6 +177,32 @@ class EntityManager $this->_conn->beginTransaction(); } + /** + * Executes a function in a transaction. + * + * The function gets passed this EntityManager instance as an (optional) parameter. + * + * {@link flush} is invoked prior to transaction commit. + * + * If an exception occurs during execution of the function or flushing or transaction commit, + * the transaction is rolled back, the EntityManager closed and the exception re-thrown. + * + * @param Closure $func The function to execute transactionally. + */ + public function transactional(Closure $func) + { + $this->_conn->beginTransaction(); + try { + $func($this); + $this->flush(); + $this->_conn->commit(); + } catch (Exception $e) { + $this->close(); + $this->_conn->rollback(); + throw $e; + } + } + /** * Commits a transaction on the underlying database connection. * diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 7336150e7..6bf7d6691 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1290,6 +1290,8 @@ class UnitOfWork implements PropertyChangedListener * @return object The managed copy of the entity. * @throws OptimisticLockException If the entity uses optimistic locking through a version * attribute and the version check against the managed copy fails. + * + * @todo Require active transaction!? OptimisticLockException may result in undefined state!? */ public function merge($entity) { diff --git a/tests/Doctrine/Tests/DBAL/Functional/ConnectionTest.php b/tests/Doctrine/Tests/DBAL/Functional/ConnectionTest.php index fc231f2f3..26663fcd0 100644 --- a/tests/Doctrine/Tests/DBAL/Functional/ConnectionTest.php +++ b/tests/Doctrine/Tests/DBAL/Functional/ConnectionTest.php @@ -59,6 +59,16 @@ class ConnectionTest extends \Doctrine\Tests\DbalFunctionalTestCase $this->_conn->rollback(); $this->assertEquals(0, $this->_conn->getTransactionNestingLevel()); } + + $this->assertEquals(0, $this->_conn->getTransactionNestingLevel()); + try { + $this->_conn->transactional(function($conn) { + $conn->executeQuery("select 1"); + throw new \RuntimeException("Ooops!"); + }); + } catch (\RuntimeException $expected) { + $this->assertEquals(0, $this->_conn->getTransactionNestingLevel()); + } } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php index 3ac3de8eb..f5d69494d 100644 --- a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php @@ -643,9 +643,10 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase $address->zip = '12345'; $user->setAddress($address); - - $this->_em->persist($user); - $this->_em->flush(); + + $this->_em->transactional(function($em) use($user) { + $em->persist($user); + }); $this->_em->clear(); //$this->_em->getConnection()->getConfiguration()->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger); @@ -661,7 +662,6 @@ class BasicFunctionalTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals('Germany', $address2->country); $this->assertEquals('Berlin', $address2->city); $this->assertEquals('12345', $address2->zip); - } //DRAFT OF EXPECTED/DESIRED BEHAVIOR