proxyfabio 24 августа 2015 0 5
В этом или прошлом году, я уже и не помню когда, проводилась конференция IT Global Meetup. И что-то меня дернуло туда поехать с докладом.

На тот момент я участвовал в разработке сложного ПО на базе MODX. Это и стало стимулом той поездки; т.к. за время работы над проектом были обнаружены некоторые «тонкости» работы движка, обнаружить которые на рядовом проекте представляется проблематичным.

Опытные господа и внимательные читатели возразят: «зачем разрабатывать сложный проект на MODX, когда берем… бла-бла-бла». В силу многих факторов мы разрабатываем на MODX.

Тема доклада, если не ошибаюсь, «Псевдотестирование в MODX». О нем, да и о тестировании в MODX, я еще напишу как-нибудь.

В этой статье я хотел бы коснуться одного крайне интересного момента, о котором я вскользь упомянул на том докладе. Речь коснется транзакций и ошибок валидации.


Итак, приступим.

Думаю, что многие знакомы с механизмом сохранения связанных объектов в MODX. Подробнее читаем здесь: https://modxclub.ru/topics/oop-metodyi-xpdoobject-set()-i-xpdoobjectset()-1204.html.

Суть в том, можно описать связанные объекты на уровне карты объекта в MODX. Если мы при сохранении объекта, у которого есть описание связанных объектов в карте, «привяжем» к нему дочерние объекты, произойдет сохранение как дочерних (причем сохранение идет по всему дереву связанных объектов), так и основного объекта.

Это если кратко, на пальцах. Механизм мощный, можно сказать — магия. Однако не все так просто.

Первый момент — ограничение. Мы настраиваем связи между объектами, на основе которых происходит сохранение. Однако бывают случаи, когда сценарий предусматривает создание объектов, меж которыми нет очевидной связи. Здесь этот механизм нам не поможет.

Второй момент — ошибки при сохранении.Вот это самый коварный момент. Наткнулся на него чисто случайно. Связан он с механизмами валидации объектов на уровне мапы (это и было темой доклада).

В xpdo существует механизм описания правил валидации данных. Своего рода юнит-тесты для объектов. При сохранении объекта валидатор дергает их (правила валидации) из карты и проверяет полученные данные. Если тест не проходит, то сохранение не завершается успехом.

Так вот. Помню, как вчера. Сижу я и отлаживаю один сценарий. И почему-то сохранение не проходит, хотя должно. Ошибок никаких.

В итоге, в цепочке связанных объектов (3 шт) один не проходил валидацию из-за указанного правила и не обновлялся. И вроде бы все ничего, но прочие объекты обновлялись.

Беда с консистентностью данных налицо. Это и стало поводом ввода механизма транзакций.

Работает это так: при работе с БД можно инициализировать транзакцию. После запуска, БД отслеживает произведенные изменения. В какой-то момент мы должны изменения либо принять, либо откатить. Откат возвращает нас к состоянию БД на момент старта транзакции, принятие подтверждает изменения.

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

Ниже привожу пример процессора, реализующего функционал транзакций (inspired by Fi1osof):
<?php
class modWebTransactionProcessor extends modObjectProcessor
{
    public function process()
    {
        $startTransaction = false;
        #

        /**
         * transaction start
         */
        if (!$this->modx->pdo->inTransaction())
        {
            $this->modx->beginTransaction();
            $startTransaction = true;
        }
        /**
         * here we call parent::process for the current instance
         */
        $response = $this->_parentprocess();
        #

        /**
         * if we have errors
         */
        $errors = $this->modx->error->getErrors(true);
        if (!count($errors))
        {
            if ($startTransaction)
            {
                $this->modx->commit();
            }
            return $response;
        }
        # else

        /**
         *  if transaction was initialized at current processor instance
         */
        if ($startTransaction)
        {
            $this->modx->rollBack();
        }
        #

        /**
         * msg
         */
        $msg = $response['message'];
        if ($response['errors'])
        {
            $msg = $msg ? $msg : $this->modx->lexicon('error');
        }
        #
        return $this->failure($msg);
    }
}
return 'modWebTransactionProcessor';


Работает так: если процессор принят к исполнению и транзакции еще нет, то мы стартуем. Если нет ошибок в процессе работы, то транзакция завершится успехом. Произошли ошибки — откат.

Этот решит проблемы с консистентностью, однако все еще не понятно отсутствие информации об ошибках.
Пришлось покопаться в сорцах движка и вот что обнаружилось.

Выше я упоминал что при сохранении объекта сохраняются связанные объекты (метод сохранения объекта вызывается два раза. Служит для корректной установки значений полей по которым связываются объекты. Об этом много писал Николай). Это значит, что для связанных объектов метод сохранения будет вызываться.

Интерес представляет следующий кусок метода сохранения: https://github.com/modxcms/revolution/blob/master/core/xpdo/om/xpdoobject.class.php#L1344-L1348.

Мы видим, что если валидатор не «одобряет» сохранение, то ничего не происходит…

Радости не было предела: ) Пришлось патчить ядро. Вот код:
if ($this->getOption(xPDO::OPT_VALIDATE_ON_SAVE)) {
    if (!$this->validate()) {
        $msgs = $this->_validator->getMessages();
        $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, print_r($msgs,1));
        foreach($msgs as $msg){
            if(!$this->xpdo->error->hasError()){
                $this->xpdo->error->addField($msg['field'],$msg['message']);
            }else{
                foreach($this->xpdo->error->getErrors(true) as $error){
                    if($error['id'] !== $msg['field'] or $error['msg'] !== $msg['message']){
                        $this->xpdo->error->addField($msg['field'],$msg['message']);

                    }
                }
            }
        }
        return false;
    }
}


Мы ловим ошибки исполнения и сохраняем их в объекте работы с ошибками, который един для окружения. В процессоре, который реализует работу с транзакциями, мы ловим эти ошибки и рубим обновление данных.

Эти 2 изменения позволили решить эту «случайно обнаруженную» проблему.

Спасибо за внимание.

P.S.: я обещал написать об этом достаточно давно, но все не было времени. Приношу свои извинения. Надеюсь, что не зря старался: )

Edit: На таблицах MyISAM транзакции не работают в целом (читаем про механизм транзакций). При создании таблиц надо выбирать InnoDB. Также стоит отметить, что возникнут проблемы с методом getIterator.
5 комментариев
R
Realetive 27 августа 2015г в 12:23 #
Огромное, безграничное спасибо!
R
Realetive 27 августа 2015г в 12:54 #
Аудиозапись с доклада на IT Global MeetUp, предоставленная Александром Кирилловым, за что ему большое спасибо.

yadi.sk/d/ce2fJra5fi3BX
Fi1osof1
Fi1osof 27 августа 2015г в 21:05 #
Спасибо!

Лучше это прям отдельным топиком написать. В комментариях затеряется.
R
Realetive 28 августа 2015г в 11:54 #
А может, Сергей просто добавит файл в свою статью? Или ты, Николай? Если сделать embed, то вообще отлично!
Fi1osof1
Fi1osof 28 августа 2015г в 16:36 #
Не, тогда уж ссылки достаточно.
Авторизуйтесь или зарегистрируйтесь (можно через соцсети ), чтобы оставлять комментарии.