При добавлении нового функционала в один проект на PHP без фреймворка, возникла необходимость использования транзакций (используется MySQL c InnoDB и PHP 5.4 с MYSQLi).
В проекте по умолчанию autocommit
установлен в true
. Выключить его для всего проекта нельзя. Соответственно первой мыслью было перед выполнением SQL-запроса отключать autocommit
, а после всех действий (плюс commit
или rollback
в конце), включать autocommit
снова.
Но такой подход сразу оказался несостоятельным, так как обычно необходимо выполнять последовательно несколько методов, в которых делаются запросы и, если в каком-то из методов возникает исключение, делать rollback
. Если же делать commit
в каждом методе, то изменения будут фиксироваться раньше, чем выполнятся все запросы.
Другой вариант — отключать и включать autocommit
после выполнения каждой связанной группы методов. Условный код (действие происходит в классе):
public function save() { $result = $this->db->update(...); //ошибка может быть не только из-за неверного запроса, но и в процессе валидации и пр. if (!$result) throw new Exception('Error while saving'); } public function append_log() { $result = $this->db->insert(...); if (!$result) throw new Exception('Error while append'); } public function add() { $this->db->autocommit(false); try { $this->save(); $this->append_log(); $this->db->commit(); } catch (Exception $e) { $this->db->rollback(); } $this->db->autocommit(true); }
Но тут возникают две проблемы:
- Писать такое в каждом методе не очень хочется
- Что, если в каком-то из методов (
save()
илиappend_log()
) будет также исполняться несколько последовательных запросов, которые надо объединить в транзакцию? Тогда придётся определять, отключали или нетautocommit
, и выполнятьcommit
в зависимости от этого, так как если выполнитьcommit
, родительские изменения тоже будут сохранены.
Нужно сделать так, чтобы код проверки и фиксирования изменений выполнялся вокруг метода без нашего участия.
public function transaction(callable $block) { $exception = null; if ($need_to_off = $this->isAutocommitOn()) $this->mysqli->autocommit(false); try { $block(); } catch (Exception $e) { $exception = $e; } if ($need_to_off) { if ($exception == null) { $this->db->mysqli->commit(); } else { $this->db->mysqli->rollback(); } } if ($exception) throw $exception; } public function isAutocommitOn() { if ($result = $this->db->mysqli->query("SELECT @@autocommit")) { $row = $result->fetch_row(); $result->free(); } return isset($row[0]) && $row[0] == 1; }
Мы посылаем методу transaction()
наш код внутри анонимной функции. Если autocommit
включен, transaction
его отключает, затем выполняет анонимную функцию. В зависимости от результата делает commit
или rollback
, а затем заново включает autocommit
. Если же autocommit
уже выключен, то просто выполняется анонимная функция — об autocommit заботятся где-то в другом месте.
Пример использования:
public function save_all() { $this->transaction(function(){ $this->save(); $this->append_log(); }); }
P.S.: $this
в замыканиях можно использовать, начиная с PHP версии 5.4
ссылка на оригинал статьи http://habrahabr.ru/post/185320/
Добавить комментарий