diff --git a/lib/DBAbstract.php b/lib/DBAbstract.php index a539b7f..8bd9248 100644 --- a/lib/DBAbstract.php +++ b/lib/DBAbstract.php @@ -30,6 +30,13 @@ abstract class DBAbstract implements DBInterface * @var int */ protected $transactions = 0; + + /** + * 事务链中还应继续执行回滚 + * 在嵌套事务中,开启多少次事务就应该执行多少次回滚 + * @var int + */ + protected $rollbackAgain = false; /** * 记录当前请求执行的SQL查询语句 diff --git a/lib/Finder.php b/lib/Finder.php index 2276e48..3a4ec5c 100644 --- a/lib/Finder.php +++ b/lib/Finder.php @@ -82,8 +82,6 @@ public function fetchAll($offset = 0, $rows = 0) } else { $opts['limit'] = "{$offset}"; } - } else { - throw new Exception('param error, one of the params must be greater than 0'); } $sql = QueryBuilder::optionsToSql($opts); diff --git a/lib/adapter/MySQLi.php b/lib/adapter/MySQLi.php index 19adb74..334f587 100644 --- a/lib/adapter/MySQLi.php +++ b/lib/adapter/MySQLi.php @@ -69,11 +69,19 @@ public function beginTransaction() */ public function commit() { + if ($this->rollbackAgain) { + throw new \wf\db\Exception('Please throw exception after rollback in last catch block'); + } + --$this->transactions; if($this->transactions == 0 && false === $this->mysqli->commit()) { throw new \wf\db\Exception('transaction commit error: ' . $this->getLastErr()); } + + if ($this->transactions < 0) { + $this->transactions = 0; + } } /** @@ -201,9 +209,17 @@ public function setAutoCommit($isAutoCommit = false) */ public function rollback() { - --$this->transactions; - - if ($this->transactions <= 0 && false === $this->mysqli->rollback()) { + + // 事务链中一旦出现异常,为避免事务链中异常后面的SQL还执行,应返回到第一个事务再回滚 + if ($this->transactions -- > 1) { + $this->rollbackAgain = true; + return; + } + + $this->rollbackAgain = false; + + // 出现异常立即回滚,回滚后再出现commit则应该抛出异常 + if (false === $this->mysqli->rollback()) { throw new \wf\db\Exception('transaction rollback error: '.$this->getLastErr()); } } diff --git a/lib/adapter/PDOMySQL.php b/lib/adapter/PDOMySQL.php index 8416bd2..8a6976a 100644 --- a/lib/adapter/PDOMySQL.php +++ b/lib/adapter/PDOMySQL.php @@ -72,11 +72,19 @@ public function beginTransaction() */ public function commit() { + if ($this->rollbackAgain) { + throw new \wf\db\Exception('Please throw exception after rollback in last catch block'); + } + --$this->transactions; if($this->transactions == 0 && false === $this->dbh->commit()) { throw new \wf\db\Exception('transaction commit error: ' . $this->getLastErr()); } + + if ($this->transactions < 0) { + $this->transactions = 0; + } } /** @@ -211,9 +219,16 @@ public function setAutoCommit($isAutoCommit = false) */ public function rollback() { - --$this->transactions; - - if ($this->transactions <= 0 && false === $this->dbh->rollback()) { + // 事务链中一旦出现异常,为避免事务链中异常后面的SQL还执行,应返回到第一个事务再回滚 + if ($this->transactions -- > 1) { + $this->rollbackAgain = true; + return; + } + + $this->rollbackAgain = false; + + // 出现异常立即回滚,回滚后再出现commit则应该抛出异常 + if (false === $this->dbh->rollback()) { throw new \wf\db\Exception('transaction rollback error: ' . $this->getLastErr()); } } diff --git a/test/MySQLiTest.php b/test/MySQLiTest.php index 8b17e34..7c58a13 100644 --- a/test/MySQLiTest.php +++ b/test/MySQLiTest.php @@ -14,7 +14,7 @@ /** * PDOMySQL test case. */ -class MySQLTest extends PHPUnit_Framework_TestCase +class MySQLiTest extends PHPUnit_Framework_TestCase { /** * @@ -203,5 +203,73 @@ public function testRollBack() $lastStr = $this->mySQLi->getColumn("SELECT str FROM wk_test_table ORDER BY id DESC"); $this->assertEquals($uniqe, $lastStr); } + + + /** + * 测试嵌套事务 + */ + public function testNestedTrans() + { + $uniqe = uniqid(); + $this->insertRow($uniqe); + + try { + $trans = $this->mySQLi->beginTransaction(); + $this->insertRow(); + $this->insertRow(); + + try { + $trans = $this->mySQLi->beginTransaction(); + $this->insertRow(); + $this->insertRow(); + $trans->commit(); + } catch (\wf\db\Exception $e) { + $trans->rollback(); + } + + try { + $trans = $this->mySQLi->beginTransaction(); + $this->insertRow(); + $this->insertRow(); + + try { + $trans = $this->mySQLi->beginTransaction(); + $this->insertRow(); + $this->insertRow(); + + // 在此出现异常 + throw new \wf\db\Exception('~'); + + $trans->commit(); + } catch (\wf\db\Exception $e) { + $trans->rollback(); + } + + // 应避免:事务因异常关闭,因上面回滚后没有再抛出异常,导致继续执行这里 + $this->insertRow(); + + $trans->commit(); + } catch (\wf\db\Exception $e) { + $trans->rollback(); + } + + $trans->commit(); + } catch (\wf\db\Exception $e) { + $trans->rollback(); + } + + // 检测事务没有添加成功(最后添加的是事务开启前的) + $lastStr = $this->mySQLi->getColumn("SELECT str FROM wk_test_table ORDER BY id DESC"); + $this->assertEquals($uniqe, $lastStr); + + + // 检查事务已正常关闭 + $uniqe = uniqid(); + $this->insertRow($uniqe); + $lastStr = $this->mySQLi->getColumn("SELECT str FROM wk_test_table ORDER BY id DESC"); + $this->assertEquals($uniqe, $lastStr); + + print 'testNestedTrans done'; + } } diff --git a/test/PDOMySQLTest.php b/test/PDOMySQLTest.php index 9ad145c..f563e8d 100644 --- a/test/PDOMySQLTest.php +++ b/test/PDOMySQLTest.php @@ -14,48 +14,48 @@ /** * PDOMySQL test case. */ -class PDOMySQLTest extends PHPUnit_Framework_TestCase +class PDOMySQLTest extends PHPUnit_Framework_TestCase { /** - * + * * @var \wf\db\DBInterface */ private $pDOMySQL; - + /** * Prepares the environment before running a test. */ - protected function setUp() + protected function setUp() { - parent::setUp (); - + parent::setUp(); + $cfg = array( // 'default' => array( - 'class' => '\\wf\\db\\adapter\\PDOMySQL', // MySQLi|PDOMySQL - 'host' => '127.0.0.1', // 本机测试 - 'port' => '3306', // 数据库服务器端口 - 'name' => 'test', // 数据库名 - 'user' => 'root', // 数据库连接用户名 - 'pass' => '123456', // 数据库连接密码 - 'tablePrefix' => 'wk_', // 表前缀 - 'debug' => 1, + 'class' => '\\wf\\db\\adapter\\PDOMySQL', // MySQLi|PDOMySQL + 'host' => '127.0.0.1', // 本机测试 + 'port' => '3306', // 数据库服务器端口 + 'name' => 'test', // 数据库名 + 'user' => 'root', // 数据库连接用户名 + 'pass' => '123456', // 数据库连接密码 + 'tablePrefix' => 'wk_', // 表前缀 + 'debug' => 1, ), // 可主从分离 'slave' => array( - 'class' => '\\wf\\db\\adapter\\PDOMySQL', // MySQLi|PDOMySQL - 'host' => '127.0.0.1', // 本机测试 - 'port' => '3306', //=> - 'name' => 'test', // 数据库名 - 'user' => 'root', // 数据库连接用户名 - 'pass' => '123456', // 数据库连接密码 - 'tablePrefix' => 'wk_', // 表前缀 - 'debug' => 1, + 'class' => '\\wf\\db\\adapter\\PDOMySQL', // MySQLi|PDOMySQL + 'host' => '127.0.0.1', // 本机测试 + 'port' => '3306', //=> + 'name' => 'test', // 数据库名 + 'user' => 'root', // 数据库连接用户名 + 'pass' => '123456', // 数据库连接密码 + 'tablePrefix' => 'wk_', // 表前缀 + 'debug' => 1, ), ); $this->pDOMySQL = new $cfg['default']['class']($cfg['default']); - + // 创建测试表 $sql = "CREATE TABLE IF NOT EXISTS `wk_test_table` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT , @@ -63,36 +63,36 @@ protected function setUp() PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci;"; $this->pDOMySQL->query($sql); - + $tableInfo = $this->pDOMySQL->getTableSchema('wk_test_table'); print_r($tableInfo); } - + /** * Cleans up the environment after running a test. */ - protected function tearDown() + protected function tearDown() { - parent::tearDown (); + parent::tearDown(); $sql = "DROP TABLE IF EXISTS wk_test_table"; $this->pDOMySQL->query($sql); } - + /** * Constructs the test case. */ - public function __construct() + public function __construct() { - + } - - private function insertRow($val = '') + + private function insertRow($val = '') { $val = $val ? $val : date('Y-m-d H:i:s'); $sql = "INSERT INTO wk_test_table (str) VALUE ('{$val}')"; $this->pDOMySQL->query($sql); } - + /** * Tests PDOMySQL->lastInsertId() */ @@ -100,44 +100,44 @@ public function testLastInsertId() { $this->insertRow(); $lastInsertId = $this->pDOMySQL->lastInsertId(); - + $this->assertNotEmpty($lastInsertId); } - + /** * Tests PDOMySQL->query() */ public function testQuery() { // TODO Auto-generated PDOMySQLTest->testQuery() - $this->markTestIncomplete ( "query test not implemented" ); - + $this->markTestIncomplete("query test not implemented"); + $this->pDOMySQL->query(/* parameters */); } - + /** * Tests PDOMySQL->exec() */ - public function testExec() + public function testExec() { // TODO Auto-generated PDOMySQLTest->testExec() - $this->markTestIncomplete ( "exec test not implemented" ); - + $this->markTestIncomplete("exec test not implemented"); + $this->pDOMySQL->exec(/* parameters */); } - + /** * Tests PDOMySQL->getAll() */ - public function testGetAll() + public function testGetAll() { $this->insertRow(); $this->insertRow(); $rows = $this->pDOMySQL->getAll("SELECT * FROM wk_test_table LIMIT 2"); - + $this->assertEquals(2, count($rows)); } - + /** * Tests PDOMySQL->getRow() */ @@ -145,27 +145,27 @@ public function testGetRow() { $uniqe = uniqid(); $this->insertRow($uniqe); - + $row = $this->pDOMySQL->getRow("SELECT * FROM wk_test_table WHERE str = '{$uniqe}'"); $this->assertNotEmpty($row); } - + /** * Tests PDOMySQL->getColumn() */ - public function testGetColumn() + public function testGetColumn() { $uniqe = uniqid(); $this->insertRow($uniqe); - + $str = $this->pDOMySQL->getRow("SELECT str FROM wk_test_table WHERE str = '{$uniqe}'"); $this->assertNotEmpty($str); } - + /** * Tests PDOMySQL->getLastErr() */ - public function testGetLastErr() + public function testGetLastErr() { $sql = "SELECT x from tb_" . uniqid(); try { @@ -176,18 +176,18 @@ public function testGetLastErr() $msg = $e->getMessage(); $this->assertEquals($lastErr, $msg); } - + /** * Tests PDOMySQL->setAutoCommit() */ public function testSetAutoCommit() { // TODO Auto-generated PDOMySQLTest->testSetAutoCommit() - $this->markTestIncomplete ( "setAutoCommit test not implemented" ); - + $this->markTestIncomplete("setAutoCommit test not implemented"); + $this->pDOMySQL->setAutoCommit(/* parameters */); } - + /** * Tests PDOMySQL->rollback() */ @@ -195,7 +195,7 @@ public function testRollBack() { $uniqe = uniqid(); $this->insertRow($uniqe); - + try { $this->pDOMySQL->beginTransaction(); $this->insertRow(); @@ -204,7 +204,72 @@ public function testRollBack() } catch (\wf\db\Exception $e) { $this->pDOMySQL->rollback(); } - + + $lastStr = $this->pDOMySQL->getColumn("SELECT str FROM wk_test_table ORDER BY id DESC"); + $this->assertEquals($uniqe, $lastStr); + } + + /** + * 测试嵌套事务 + */ + public function testNestedTrans() + { + $uniqe = uniqid(); + $this->insertRow($uniqe); + + try { + $trans = $this->pDOMySQL->beginTransaction(); + $this->insertRow(); + $this->insertRow(); + + try { + $trans = $this->pDOMySQL->beginTransaction(); + $this->insertRow(); + $this->insertRow(); + $trans->commit(); + } catch (\wf\db\Exception $e) { + $trans->rollback(); + } + + try { + $trans = $this->pDOMySQL->beginTransaction(); + $this->insertRow(); + $this->insertRow(); + + try { + $trans = $this->pDOMySQL->beginTransaction(); + $this->insertRow(); + $this->insertRow(); + + // 在此出现异常 + throw new \wf\db\Exception('~'); + + $trans->commit(); + } catch (\wf\db\Exception $e) { + $trans->rollback(); + } + + // 应避免:事务因异常关闭,因上面回滚后没有再抛出异常,导致继续执行这里 + $this->insertRow(); + + $trans->commit(); + } catch (\wf\db\Exception $e) { + $trans->rollback(); + } + + $trans->commit(); + } catch (\wf\db\Exception $e) { + $trans->rollback(); + } + + // 检测事务没有添加成功(最后添加的是事务开启前的) + $lastStr = $this->pDOMySQL->getColumn("SELECT str FROM wk_test_table ORDER BY id DESC"); + $this->assertEquals($uniqe, $lastStr); + + + // 检查事务已正常关闭 + $uniqe = uniqid(); + $this->insertRow($uniqe); $lastStr = $this->pDOMySQL->getColumn("SELECT str FROM wk_test_table ORDER BY id DESC"); $this->assertEquals($uniqe, $lastStr); }