Асинхронная индексация 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