Как стать автором
Обновить

Комментарии 13

Вы видимо не внимательно читали, в данной статье решается совершенно другая проблема — восстановление модели, после отката транзакции. Помимо это, ваш код имеет ряд недостатков:
1. Код не будет работать если инициализирована транзакция на более высоком уровне, например в контроллере.
2. Нельзя использовать вложенное сохранение, например в afterSave() использовать сохранение другой модели. Ваш код вывалит Exception.
Ок. Пункт 1 вроде уже как реализован, пункт 2 можно пофиксить никаких проблем. Накидайте таск а я потом займусь.
До чего жизнь довела. Сначала слово «откат» совсем по-другому понял. Думал потом, при чём тут Yii.
А зачем это все?
Если возникло исключение, значит при сохранении данных на уровне БД что-то пошло не так, и дальнейшее выполнение кода необходимо остановить предварительно откатив транзакцию.
Для этого в блоке catch, после rollback, вызывается перехваченное исключение throw $e. В этом случае до вьюва дело не дойдет.
В нашем случае исключение может быть вызвано не только ошибкой на уровне БД, но и по случаю передачи неверных данных с точки зрения бизнес-логики. В таком случае нужно восстановить модель и показать вьюшку со старыми данными и описанием ошибки. Вероятно для некоторых, приведенный код не показателен в отрыве от бизнес логики. Постараюсь объяснить на словах. В 1С часто используется подход (особенно в новых редакциях типовых конфигураций), когда все данные сначала пишутся в базу данных, а потом проверяются на корректность, это характерно для проведения по регистрам (что такое регистры и как мы их применили в своем проекте, я создам как-нибудь отдельный пост). Именно такой подход мы решили применить и в своем проекте, так как он очень удобен для учетных систем. Как раз это заставляет нас производить вложенные сохранения и выбрасывать пользовательские исключения после сохранения исходной модели.
«Неверные» данные нужно отсекать валидаторами. Тем более что данные в базу все равно не сохраняются, так как по исключению вы делаете rollback.
Я уже говорил, что валидаторы в нашем случае не подходят. Точнее, валидаторы мы конечно используем, но существуют случаи, когда они не могут проверить всю бизнес логику. Не хотел в данном посте описывать регистры, видимо придется. Постараюсь привести показательный пример.
Есть модель ProductMovement (перемещение товаров). Эта модель после сохранения, само собой разумеется производит запись в свою таблицу, а так же после своего сохранения производит сохранение моделей регистра ProductRest (остаток товара на складе). Это можно проиллюстрировать примерно так:
image
Представим, что мы создаем документ «Перемещение 3». Бизнес требование заключается в том что сумма по колонке кол-во всегда должно быть положительным ( SUM('кол-во') > 0 ), кроме того это требование настолько важно, что коллизий не должно быть при конкурентных сохранениях, поэтому при проверках, эту таблицу будем блокировать. Рассмотрим два решения этой задачи.

1. Ваш вариант — проверка перед сохранением, его можно реализовать валидатором.
а. Считаем кол-во, которое мы планируем вставить.
б. Блокируем таблицу регистров.
в. Считаем остаток в таблице регистров агрегатным запросом.
г. Складываем результат с планируемой суммой.
д. Валидируем итоговую сумму.
е. Снимаем блокировку.

2. Наш вариант — проверка после сохранения, его нельзя реализовать валидатором.
а. Производим запись в таблицу регистров.
б. Блокируем таблицу регистров.
в. Считаем остаток в таблице регистров агрегатным запросом.
г. Валидируем итоговую сумму.
д. Снимаем блокировку.

А теперь представим себе, что в реальности в регистре несколько миллионов записей и десяток полей по которым нужно сгруппировать при агрегатном запросе и при вычислении планируемых и итоговых сумм. А так же самих регистров участвует в сохранении несколько.
Обратите внимание, что в первом варианте, блокировка таблицы вызывается раньше, а сами вычисления сложнее, к тому же они будут вызываться при любой валидации, например ajax-валидации. Все это приведет к усложнению кода, большой нагрузке на сервер и длительным блокировкам таблиц.
Агрегатный запрос на миллионах записей? Вам не жалко сервер? Вы сэкономили на одной операции, но это все равно не разгрузит сервер. В подобных случаях надо избавляться от агрегатных запросов, например, хранить итоговую сумму в отдельной таблице. Да, это дублирование данных, но в то же самое время это разгрузит сервер и не нужно будет таких извращений. Вариант с переопределением BaseActiveRecord тоже плох. Что если вам понадобится сделать так чтоб модели могли работать с двумя разными субд(sqlite и mysql), причем для каждой из субд только часть моделей должна восстанавливать своё состояние при откате. В таком случае лучше пользоваться событиями или поведением(о чем я написал в первом комментарии).
Таблица с итоговыми данными — не разгрузит сервер, потому что для того чтобы данные туда положить, нужно выполнить тот же агрегатный запрос, и производить эту агрегацию так же часто, как и в нашем случае, ведь на этих данных планируется построить бизнес логику. Сервер разгрузит, сворачивание регистров, что мы и планируем делать в своем приложении.

По поводу поведений, что значит, модель не должна восстанавливаться после отката транзакции, неужели бывают юзкейсы, когда плохо, что модель не отражает актуального состояния???
1. Видимо я не совсем понял вашу задачу. Как я понял, если у нас есть поле в котором агрегатная информация, то нам достаточно прибавить к нему нужное значение и выполнить валидацию. Зачем перезапускать агрегатный запрос?
2. Как уже писали выше, откатывание значений в моделе после транзакции нужно лишь в единичных случаях. Невалидные данные не должны доходить до транзакции(иначые юзер не сможет их исправить). А то получится, что юзер сохранил невалидные данные, ему никак не сообщилось об этом, он ожидает что запись сохранилась, а вот те на — она не сохранилась при транзакции. Юзер хочет видеть сообщение об ошибке, как вы ему его покажите? Или же вы решили использовать сохранение в транзакции в качестве валидатора? Зачем, если есть стандартный механизм валидации? Нужен свой валидатор, который будет работать только при определенном сценарии(не аякс или же можно в самом валидаторе проверять мол если аякс запрос то ничего не делать), и, если все остальные данные корректны. Это избавит от аякс валидации. По варианту 1, пункт «а», что выше. Не совсем понял почему его нет в варианте 2? Но, даже если в варианте 1 на одну операцию больше, по-любому это упростит код и его станет легче сопровождать. В любом случае, вы делаете валидацию на фазе сохранения, после фазы валидации, что не ожидаемо и, со временем, может стать неконтролируемым поведением. Однако, сама по себе, идея восстанавливать состояние модели при откате транзакции(не все поля а только isNewRecord и pk) очень полезная, только в данной ситуации это не нужно.
Жаль, что не указали в статье, как Вы создаете транзакцию класса DbTransaction. Выражение вида:
$transaction = $model->getDbConnection()->beginTransaction();

не объясняет появление транзакций нового класса.

Кроме этого, предлагаю в функции rollback выполнять не foreach для возврата состояний моделей, а for с обратным перебором массива:
$cnt = count($this->_models);
for ($i = $cnt - 1; $i >= 0; $i--)
    $this->_models[$i]->restoreState();

По-моему это более правильно, вернуть состояние моделей в порядке, обратном порядку их сохранения.

В целом, спасибо за статью. Пригодилось.
Да, согласен. Стоило указать, что в нашем приложении переопределен класс CDbConnection, а в нем метод beginTransaction
public function beginTransaction()
{
    Yii::trace('Starting transaction','system.db.CDbConnection');
    $this->setActive(true);
    $this->getPdoInstance()->beginTransaction();
    return $this->_transaction = new DbTransaction($this);
}
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории