Транзакции в goDB

Mysqli предоставляет следующие методы для реализации транзакций: autocommit(), commit() и rollback(). Кроме этого, конечно, остаётся возможность реализовывать их непосредственно через SQL-запросы.

goDB дополняет эти возможности своими методами:


int   transactionBegin([$saveAC = false]);       // начало транзакции
bool  transactionCommit(void);                   // сохранение транзакции
bool  transactionRollback([bool $throw = false); // откат транзакции
mixed transactionRun(callback $callback [, $saveAC = false]); // выполнение функции в транзакции
bool  transactionFailed(void);                   // не провалилась ли транзакция
int   transactionLevel(void);                    // получить текущий уровень вложенности
bool  inTransaction(void);                       // находимся ли мы внутри транзакции
bool  transactionClose([bool $commit = false]);  // закрыть транзакцию
			

Следует понимать, что goDB, как и mysqli не делает ничего невозможного — если движок таблицы не поддерживает транзакции, то их и не будет.

Простые транзакции

Набор методов обычен: BEGIN, COMMIT и ROLLBACK.


$db->transactionBegin();
// Внутри транзакции
$db->query($query1);
$db->query($query2);
$db->query($query3);
if ($condition) {
	$db->transactionCommit(); // Сохранить транзакцию
} else {
	$db->transactionRollback(); // Откатить транзакцию
}
// Уже вышли из транзакции
$db->query($query4);
			

Вложенные транзакции


/**
 * Какая-то функция, выполняющая транзакцию
 */			
function func1() {
	$db->transactionBegin();
	...
	$db->transactionCommit();
}			

/**
 * Ещё одна функция, выполняющая транзакцию и вызывающая внутри неё, предыдущую
 */
function func2() {
	$db->transactionBegin();
	...
	func1();
	...
	$db->transactionCommit();
}
			

Как видно, внутри одной транзакции могут быть вложенные транзакции. Причём вложенные могут не подозревать о том, что они вложенные.

goDB отслеживает уровень вложенности транзакций и вызывает непосредственное сохранение данных только при коммите на самом верхнем уровне.

Откат вложенных транзакций

Очевидно, что ошибка в любой части транзакции означает ошибку всей транзакции. Следовательно откат на любом уровне вложенности, должен вести к откату всей транзакии верхнего уровня. goDB предоставляет два способа для подобного отката.

Тихий откат


function func1() {
	$db->transactionBegin();
	...
	if (!$condition) {
		$this->transactionRollback();
		return;
	}
	...
	$db->transactionCommit();
}			

$db->transactionBegin();
func1();
func2();
func3();
$db->transactionCommit();
			

Если где-то во вложенной транзакции вызывается transactionRollback(), код продолжает выполняться дальше, однако все запросы (в том числе подготовленные и мультизапросы) возвращают False. Достижение же transactionCommit() верхнего уровня ведёт на самом деле к rollback'у и выходу из транзакции.

С помощью transactionFailed() внутри транзакции можно узнать не помечена ли она уже, как провалившаяся.

Откат с исключением

transactionRollback() с аргументом true, сбрасывает весь стек транзакций и генерирует исключение goDBExceptionTransactionRollback.


function func1() {
	$db->transactionBegin();
	...
	if (!$condition) {
		$this->transactionRollback(true);
	}
	...
	$db->transactionCommit();
}			

try {
	$db->transactionBegin();
	func1();
	func2();
	func3();
	$db->transactionCommit();
} catch (goDBExceptionTransactionRollback $e) {
}
			

Если нет уверенности, что мы находимся на верхнем уровне, лучше добавить проверку:


try {
	$level = $db->transactionBegin(); // begin возвращает уровень вложенности
	func1();
	func2();
	func3();
	$db->transactionCommit();
} catch (goDBExceptionTransactionRollback $e) {
	if ($level > 1) {
		throw $e;
	}
}
			

goDB-методы vs mysqli-методы

Вне транзакции организованной с помощью transactionBegin(), методы mysqli (autocommit() и др.) ведут себя также, как и обычно. Внутри транзакции — несколько по другому.

Отличное поведение основывается на тех же предположениях про вложенные транзакции.

autocommit() и commit() не имеют никакого эффекта.

rollback() вызывает transactionRollback() без исключения.

Следует заметить, что goDB-транзакции исходят из предположения, что autocommit изначально True. То есть по выходе из транзакций он будет установлен обратно в True.

Можно установить аргумент $saveAC в True, тогда значение автокоммита будет запоминаться. Однако, это производится через лишний запрос к базе, так что решите нужно ли вам это.


int  goDB::transactionBegin([bool $saveAc]);
bool goDB::transactionRun(callback $callback [, bool $saveAc]);
			

Кстати, введена дополнительная функция getAutocommit() возвращающая значение автокоммита.

transactionRun: выполнение функции в транзакции


$db->transactionRun(
	function() use ($db) {
		$db->query($query1);
		$db->query($query2);
		$db->query($query3);
		return true;
	}
);			
			

Возвращение callback-функцией любого значения, приводящегося к True, расценивается, как корректное выполнение и приводит к сохранению транзакции. Это же значение возвращается и из transactionRun(). Значения же, приводящиеся к False, приводят к откату транзакции.

В случае возникновения в callback-функции исключений, transactionRun ведёт себя следующим образом:

transactionClose: закрытие транзакции

transactionClose() вызывает выход из транзакции, на каком бы её уровне мы не находились. По умолчанию приводит к откату всех уже сделанных изменений. Указание же True в качестве единственного аргумента, приводит к их сохранению.

Следует использовать с крайней осторожностью. Например, так:


try {
	func();
} catch (RuntimeException $e) {
	$db->transactionClose();
}
			

Внутри вызова func() могут вызываться транзакции любого уровня вложенности. В то же время там может быть выброшено какое-то исключение, которое, однако, не должно привести к завершению приложения. На уровне, на котором оно будет перехвачено уже не должно быть никаких транзакций, но счётчик вложенности останется на том уровне, когда произошло исключение. Поэтому просто сбрасываем его: transactionClose().

© Григорьев Олег aka vasa_c, 2006—2010