PHP, PDO & Nested Transactions
I’ve been using PDO as my database library and it works reasonably well (as long as you remember it’s not a full blown database abstraction library), however recently I needed to use nested transactions to ensure that the database remains consistent while doing a series of SQL statements.
Unfortunately PDO does not support nested transactions although PostgreSQL and MySQL do. I decided to extend the PDO class to support nested transactions while also using PDO to keep track of the first transaction. I came up with the following class (released under the GNU General Public License, Version 3):-
class MyPDO extends PDO {
// Database drivers that support SAVEPOINTs.
protected static $savepointTransactions = array("pgsql", "mysql");
// The current transaction level.
protected $transLevel = 0;
protected function nestable() {
return in_array($this->getAttribute(PDO::ATTR_DRIVER_NAME),
self::$savepointTransactions);
}
public function beginTransaction() {
if(!$this->nestable() || $this->transLevel == 0) {
parent::beginTransaction();
} else {
$this->exec("SAVEPOINT LEVEL{$this->transLevel}");
}
$this->transLevel++;
}
public function commit() {
$this->transLevel--;
if(!$this->nestable() || $this->transLevel == 0) {
parent::commit();
} else {
$this->exec("RELEASE SAVEPOINT LEVEL{$this->transLevel}");
}
}
public function rollBack() {
$this->transLevel--;
if(!$this->nestable() || $this->transLevel == 0) {
parent::rollBack();
} else {
$this->exec("ROLLBACK TO SAVEPOINT LEVEL{$this->transLevel}");
}
}
}
This code will only attempt to use the SAVEPOINT code if you’re using a database driver that supports it (it should probably version check the database server) this then means that in your code you can do things like:-
$pdo = new MyPDO(DB_DSN, DB_USER, DB_PASS);
$pdo->beginTransaction();
try {
$pdo->exec(...);
$pdo->exec(...);
$pdo->beginTransaction();
try {
$pdo->exec(...);
$pdo->exec(...);
$pdo->exec(...);
$pdo->commit();
} catch(PDOException $e) {
// If this statement fails, rollback...
// NOTE: This will only rollback statements made in the
// inner try { block and not the outer one.
$pdo->rollBack();
}
$pdo->commit();
} catch (PDOException $e) {
$pdo->rollBack();
}
NB: I’ve tweaked the code slightly when transferring it to my blog and I haven’t tested it, so there could be some minor errors – please leave comments if you spot any. Thanks!
