Рассмотрим следующий сценарий:

  • Я открываю новое соединение с MySQL, используя расширение mysqli
  • Я начинаю сделку
  • Я делаю несколько запросов к базе данных MySQL и к некоторым вычислениям php
  • Либо MySQL, либо php могут вызывать некоторые исключения
  • Я совершаю транзакцию

(псевдо) код:

$sql = new mysqli();
...
{ //some scope
    $sql->query("START TRANSACTION");
    ... 
    if (something)
        throw Exception("Something went wrong");
    ...
    $sql->query("COMMIT"); // or $sql->commit()
} //leave scope either normally or through exception
...
$sql->query( ... ); // other stuff not in transaction

Если все вышеперечисленное находится в каком-то большом блоке try catch (или, возможно, даже исключение не перехвачено), транзакция останется незавершенной. Если я затем попытаюсь еще поработать с базой данных, эти действия будут принадлежать транзакции.

Мой вопрос: существует ли какой-то возможный механизм, который позволил бы мне автоматически отправлять $sql->rollback(), когда я выхожу из области создания транзакции ненормальным образом?


Некоторые примечания:

  • mysqli::autocommit - это не то же самое, что транзакция. Это просто настройка для включения или выключения автоматической фиксации. Эквивалентно MySQL SET autocommit.
  • mysqli::begin_transaction не задокументирован, и я не знаю, что его точное поведение. Будет ли он вызывать rollback, например, когда объект mysqli умирает?
0
CygnusX1 6 Мар 2015 в 21:42

4 ответа

Лучший ответ

Вы не можете автоматически выполнять откат при исключении, но с небольшим кодом вы можете делать то, что хотите. Даже если вы уже находитесь в блоке try-catch, вы можете вложить их в свой раздел транзакций, например:

try {
    $sql->query("START TRANSACTION");
    ... 
    if (something)
        throw new PossiblyCustomException("Something went wrong");
    ...
    $sql->query("COMMIT"); 
} catch (Exception $e) { //catch all exceptions
    $sql->query("ROLLBACK");
    throw $e; //rethrow the very same exception object
}

Даже если вы используете самый общий catch Exception, фактический тип исключения известен. Когда вы бросаете повторно, PossiblyCustomException все еще может поймать его позже. Таким образом, этот новый блок try-catch не повлияет на всю вашу уже имеющуюся обработку.

0
CygnusX1 6 Мар 2015 в 20:47

Таким образом, вы хотите либо зафиксировать транзакцию, либо откатить транзакцию, если возникла проблема, и есть много способов, по которым проблема могла возникнуть - либо через ошибку mysql, либо через php, генерирующий исключение. Так почему бы не установить флаг в начале транзакции, установить этот флаг, когда транзакция готова к фиксации, и не проверить состояние флага, когда процесс завершен, чтобы увидеть, нужен ли откат?

Если вы боитесь глобальных переменных, вы можете использовать для этого слишком сложный класс Singleton или какую-то статическую переменную, но вот основная идея:

function main() {
  global $commit_flag = false;
  start_transaction();
  doEverything();

  if ($commit_flag) {
    commit_transaction();
  } else {
    rollback_transaction();
  }
}

function doEverything() {
  global $commit_flag;
  try {
    /* do some stuff that may cause errors */

    $commit_flag = true;
  } catch { ... }
}
0
Roger Halliburton 6 Мар 2015 в 19:28

Вы можете обрабатывать исключения mysql, используя синтаксис DECLARE ... HANDLER, но я не считаю правильным пытаться обрабатывать их через PHP!

BEGIN
        DECLARE EXIT HANDLER FOR SQLEXCEPTION  ROLLBACK;
        START TRANSACTION;
          INSERT INTO Users ( user_id ) VALUES('1');
          INSERT INTO Users ( user_id ) VALUES('2');
        COMMIT;
END;

Более подробную информацию можно найти здесь : http://dev.mysql.com/doc/refman/5.5 /en/declare-handler.html

0
Mohamed Almasry 6 Мар 2015 в 19:11

Команда mysqli::begin_transaction такая же (объектно-ориентированная), что и ваш $sql->query("START TRANSACTION");;

Нет возможности автоматического отката при исключении.

Вы можете выполнить только комит, если все будет успешно. Тогда это будет "авто откат", если нет. Но в этом случае очень скоро у вас возникнут проблемы, если у вас будет более одной фиксации.

Итак, ваш текущий код уже очень хорош. Я бы сделал это полностью ООП:

$sql->begin_transaction();
try {
   $sql->query('DO SOMETHING');
   if(!true) {
       throw new \Exception("Something went wrong");
   }
   $sql->commit();
}
catch (\Exception exception) {
   $sql->rollback();
}

Вы также можете написать собственное исключение:

class SqlAutoRollbackException extends \Exception {
    function __construct($msg, Sql $sql) {
        $sql->rollback();
        parent::__construct($msg);
    }
}

Но поймать его все равно нужно.

1
Christian Gollhardt 6 Мар 2015 в 19:19