Асинхронная индексация sales order grid в Magento

Введение

В процессе создания и редактирования заказа в Magento происходит копирование данных основной таблицы заказов в вспомогательную для отображения списка заказов в административной части сайта. При этом основные данные заказа хранятся в таблице sales_flat_order, а данные, используемые для вывода списка заказов в таблице sales_flat_order_grid. Таблица sales_flat_order_grid дублирует часть колонок таблицы sales_flat_order вместе с данными и служит для ускорения выборки данных.

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

При мониторинге сайта в системе New Relic, можно увидеть вышеозначенную проблему примерно в таком виде.

На выделенном фрагменте видно, что вставка в таблицу sales_flat_order_grid заняла больше 27 секунд.

Асинхронные операции в Magento

Частой практикой в Magento является перенос некоторых операций в фоновый режим для увеличения скорости работы сайта, то есть выполнение их асинхронно. Обычно для этих целей используют планировщик задач cron, поддержка которого есть в Magento по умолчанию. Например, в Magento Community Edition отправка писем о заказе выполняется в фоновом режиме, а в Magento Enterprise Edition есть режим обновления индексов по расписанию, когда при изменении сущностей происходит сохранения идентификатора сущности в специальную таблицу (так называемый change log), а непосредственно индексация выполняется уже в фоне.

Асинхронная индексация sales_flat_order_grid в Magento 1 отсутствует, однако её можно реализовать самостоятельно по тому же принципу, который использует Magento Enterprise Edition для выполнения индексации по расписанию.

Добавление асинхронной индексации для sales_flat_order_grid

Для начала создадим каркас модуля, который в дальнейшем будет реализовывать нашу логику.

Создадим следующую структуру папок и файлов:

app/code/community/Vendor/GridReindex/etc/config.xml
app/code/community/Vendor/GridReindex/Helper/Data.php
app/etc/modules/Vendor_GridReindex.xml

Содержимое файла app/code/community/Vendor/GridReindex/etc/config.xml:

<?xml version="1.0"?>
<config>
    <modules>
        <Vendor_GridReindex>
            <version>0.1.0</version>
        </Vendor_GridReindex>
    </modules>
    <global>
        <helpers>
            <vendor_gridreindex>
                <class>Vendor_GridReindex_Helper</class>
            </vendor_gridreindex>
        </helpers>
    </global>
</config>
</code>

Содержимое файла app/code/community/Vendor/GridReindex/Helper/Data.php:

<?php

class Vendor_GridReindex_Helper_Data extends Mage_Core_Helper_Abstract
{}

Содержимое файла app/etc/modules/Vendor_GridReindex.xml:

<?xml version="1.0" encoding="UTF-8"?>
<config>
    <modules>
        <Vendor_GridReindex>
            <active>true</active>
            <codePool>community</codePool>
        </Vendor_GridReindex>
    </modules>
</config>

Название "Vendor" выбрано произвольно и может быть заменено на название вашей компании.

Проверим, что модуль создан правильно и включен. Для этого нужно зайти в System->Config->Advanced в административной части и убедиться, что модуль Vendor_GridReindex присутствует в списке.

Теперь нам нужно создать таблицу в базе данных, для хранения id заказов, которые требуют синхронизации с таблицей sales_flat_order_grid.

Добавим модель, resource-модель и setup resource в config.xml:

<?xml version="1.0"?>
<config>
    <modules>
        <Vendor_GridReindex>
            <version>0.1.0</version>
        </Vendor_GridReindex>
    </modules>
    <global>
        <models>
            <vendor_gridreindex>
                <class>Vendor_GridReindex_Model</class>
                <resourceModel>vendor_gridreindex_resource</resourceModel>
            </vendor_gridreindex>
            <vendor_gridreindex_resource>
                <class>Vendor_GridReindex_Model_Resource</class>
                <entities>
                    <order_grid_log>
                        <table>vendor_grid_order_grid_log</table>
                    </order_grid_log>
                </entities>
            </vendor_gridreindex_resource>
        </models>
        <helpers>
            <vendor_gridreindex>
                <class>Vendor_GridReindex_Helper</class>
            </vendor_gridreindex>
        </helpers>
        <resources>
            <vendor_gridreindex_setup>
                <setup>
                    <module>Vendor_GridReindex</module>
                </setup>
            </vendor_gridreindex_setup>
        </resources>
    </global>
</config>

Создадим файл install-скрипта app/code/community/Vendor/GridReindex/sql/vendor_gridreindex_setup/install-0.1.0.php:

<?php

/* @var $installer Mage_Core_Model_Resource_Setup */
$installer = $this;

$installer->startSetup();

/**
 * Create table 'vendor_gridreindex/order_grid_log'
 */
$table = $installer->getConnection()
    ->newTable($installer->getTable('vendor_gridreindex/order_grid_log'))
    ->addColumn('version_id', Varien_Db_Ddl_Table::TYPE_BIGINT, null, array(
        'primary'   => true,
        'unsigned'  => true,
        'identity'  => true,
        'nullable'  => false,
    ), 'Version Id')
    ->addColumn('entity_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
        'unsigned'  => true,
        'nullable'  => false,
    ), 'Order Id')
    ->setComment('Order Grid Log');
$installer->getConnection()->createTable($table);


$installer->endSetup();

После очистки кеша и открытия сайта в базе данных должна быть создана таблица vendor_grid_order_grid_log.

Затем нужно найти место в коде, где происходит копирование данных из sales_flat_order в sales_flat_order_grid. Это происходит в файле app/code/core/Mage/Sales/Model/Abstract.php в функции afterCommitCallback.

Кроме того, синхронизация данных происходит при сохранении отдельного атрибута заказа функцией saveAttribute в файле app/code/core/Mage/Sales/Model/Resource/Order/Abstract.php

В обоих случаях происходит вызов функции updateGridRecords у resource-модели заказа (абстрактный класс Mage_Sales_Model_Resource_Order_Abstract), с передачей в функцию id измененных сущностей.

Рассмотрим эту функцию:


public function updateGridRecords($ids)
{
    if ($this->_grid) {
        if (!is_array($ids)) {
            $ids = array($ids);
        }

        if ($this->_eventPrefix && $this->_eventObject) {
            $proxy = new Varien_Object();
            $proxy->setIds($ids)
                ->setData($this->_eventObject, $this);

            Mage::dispatchEvent($this->_eventPrefix . '_update_grid_records', array('proxy' => $proxy));
            $ids = $proxy->getIds();
        }

        if (empty($ids)) { // If nothing to update
            return $this;
        }
        $columnsToSelect = array();
        $table = $this->getGridTable();
        $select = $this->getUpdateGridRecordsSelect($ids, $columnsToSelect);
        $this->_getWriteAdapter()->query($select->insertFromSelect($table, $columnsToSelect, true));
    }

    return $this;
}

Перед тем как выполнить синхронизацию происходит вызов события, в которое передается proxy-объект со списком ids. Мы можем воспользоваться этим событием для нашей цели. Значение переменной $this->_eventPrefix для заказа будет равно "sales_order_resource".

Добавляем обработчик события sales_order_resource_update_grid_records в секцию frontend файла config.xml:

    <frontend>
        <events>
            <sales_order_resource_update_grid_records>
                <observers>
                    <vendor_gridreindex_log_order_ids>
                        <class>vendor_gridreindex/observer</class>
                        <method>logOrderIds</method>
                    </vendor_gridreindex_log_order_ids>
                </observers>
            </sales_order_resource_update_grid_records>
        </events>
    </frontend>

Содержимое файла app/code/community/Vendor/GridReindex/Model/Observer.php:

<?php

class Vendor_GridReindex_Model_Observer
{
    
    public function logOrderIds(Varien_Event_Observer $observer)
    {
        $proxy = $observer->getProxy();
        $ids = $proxy->getIds();
        if (!empty($ids)) {
            /** @var Mage_Core_Model_Resource $resource */
            $resource = Mage::getSingleton('core/resource');
            $connection = $resource->getConnection('write');
            $connection->insertArray(
                $resource->getTableName('vendor_gridreindex/order_grid_log'), array('entity_id'),
                $ids
            );
            $proxy->setIds(array());
        }
        return $this;
    }
}

Здесь мы сохраняем ids заказов в нашу таблицу, и обнуляем ids у proxy-объекта, для отмены стандартной синхронизации.

Теперь, при каждом сохранении заказа в таблицу vendor_grid_order_grid_log будет сохраняться id сущности, что означает необходимость обновления данных в sales_flat_order_grid. Мы добавили обработчик в секцию frontend для того чтобы в административном интерфейсе всё работало по-старому, и изменения отображались в таблице заказов незамедлительно.

Осталось выполнить синхронизацию данных по крону. Добавим новую cron-задачу в config.xml после секции global:

    <crontab>
        <jobs>
            <vendor_gridreindex_update_sales_grid>
                <schedule>
                    <cron_expr>always</cron_expr>
                </schedule>
                <run>
                    <model>vendor_gridreindex/observer::updateSalesGridByCron</model>
                </run>
            </vendor_gridreindex_update_sales_grid>
        </jobs>
    </crontab>

Параметр always говорит о том, что функция updateSalesGridByCron будет запускаться при каждом вызове magento cron, обычно это одна минута.

Добавляем функцию updateSalesGridByCron в файл app/code/community/Vendor/GridReindex/Model/Observer.php:

<?php

public function updateSalesGridByCron()
{
    /** @var Mage_Core_Model_Resource $resource */
    $resource = Mage::getSingleton('core/resource');
    $connection = $resource->getConnection('write');
    $logTable = $resource->getTableName('vendor_gridreindex/order_grid_log');

    $entityIds = $connection->fetchPairs(
        $connection->select()->from($logTable, array('version_id','entity_id'))
    );

    if (!count($entityIds)) {
        return $this;
    }
    ksort($entityIds);
    /** @var Mage_Sales_Model_Resource_Order $salesResource */
    $salesResource = Mage::getModel('sales/order')->getResource();
    $salesResource->updateGridRecords(array_unique(array_values($entityIds)));
    $connection->delete($logTable, $connection->quoteInto('version_id IN (?)', array_keys($entityIds)));
    return $this;
}

Наш обсервер достает id всех изменившихся заказов из таблицы лога, обновляет таблицу sales_flat_order_grid для этих id и удаляет их из таблицы лога. Мы получаем дополнительный прирост скорости за счёт того, что одновременно обновляем сразу несколько заказов, а не каждый по отдельности.

Содержимое файла app/code/community/Vendor/GridReindex/etc/config.xml:

<?xml version="1.0"?>
<config>
    <modules>
        <Vendor_GridReindex>
            <version>0.1.0</version>
        </Vendor_GridReindex>
    </modules>
    <global>
        <models>
            <vendor_gridreindex>
                <class>Vendor_GridReindex_Model</class>
                <resourceModel>vendor_gridreindex_resource</resourceModel>
            </vendor_gridreindex>
            <vendor_gridreindex_resource>
                <class>Vendor_GridReindex_Model_Resource</class>
                <entities>
                    <order_grid_log>
                        <table>vendor_grid_order_grid_log</table>
                    </order_grid_log>
                </entities>
            </vendor_gridreindex_resource>
        </models>
        <helpers>
            <vendor_gridreindex>
                <class>Vendor_GridReindex_Helper</class>
            </vendor_gridreindex>
        </helpers>
        <resources>
            <vendor_gridreindex_setup>
                <setup>
                    <module>Vendor_GridReindex</module>
                </setup>
            </vendor_gridreindex_setup>
        </resources>
    </global>
    <frontend>
        <events>
            <sales_order_resource_update_grid_records>
                <observers>
                    <vendor_gridreindex_log_order_ids>
                        <class>vendor_gridreindex/observer</class>
                        <method>logOrderIds</method>
                    </vendor_gridreindex_log_order_ids>
                </observers>
            </sales_order_resource_update_grid_records>
        </events>
    </frontend>
    <crontab>
        <jobs>
            <vendor_gridreindex_update_sales_grid>
                <schedule>
                    <cron_expr>always</cron_expr>
                </schedule>
                <run>
                    <model>vendor_gridreindex/observer::updateSalesGridByCron</model>
                </run>
            </vendor_gridreindex_update_sales_grid>
        </jobs>
    </crontab>
</config>

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

Асинхронная индексация гридов в Magento 2

В Magento 2 был добавлен функционал асинхронной индексации таблиц гридов, в том числе заказов. По умолчанию данный функционал отключен и его можно включить здесь Stores->Configuration->Advanced->Developer->Grid Settings->Asynchronous indexing

Заключение

В этой статье мы выполнили оптимизацию процесса создания заказа, за счёт переноса части логики в фоновой режим. Благодаря этому скорость оформления заказа увеличилась, нагрузка на базу данных снизилась. Данный приём может быть использован не только с заказами, но и во множестве других похожих случаях.

UPD 18.04.2017. Текст статьи обновлен. Вместо перезаписи модели заказа теперь используется событие sales_order_resource_update_grid_records