Magento 2: импорт продуктов прямо в базу

    В предыдущей статье я описал процесс импорта продуктов в Magento 2 обычным способом — через модели и репозитории. Обычный способ отличается весьма низкой скоростью обработки данных. На моём ноутбуке выходило примерно один продукт в секунду. В данном продолжении я рассматриваю альтернативный способ импорта продукта — прямой записью в базу, в обход стандартных механизмов Magento 2 (модели, фабрики, репозитории). Последовательность шагов, обеспечивающих импорт продуктов, может быть адаптирована под любой язык программирования, способный работать с MySQL.


    Disclaimer: В Magento есть готовый функционал по импорту данных и, скорее всего, вам его хватит. Однако если вам нужен более полный контроль за процессом импорта, не ограничивающийся подготовкой CSV-файла для того, что есть — добро пожаловать под кат.


    image


    Код, получившийся в результате написания обеих статей, можно посмотреть в Magento-модуле "flancer32/mage2_ext_demo_import". Вот некоторые ограничения, которых я придерживался, чтобы упростить код демо-модуля:


    • Продукты только создаются, не обновляются.
    • Один склад
    • Импортируются только названия категорий, без их структуры
    • Структуры данных соответствуют версии 2.3

    JSON для импорта отдельного продукта:


    {
      "sku": "MVA20D-UBV-3",
      "name": "Заглушка для пломбировки ВА47-29 IEK",
      "desc": "Обеспечение доступа к устройствам ...",
      "desc_short": "Заглушка для пломбировки ВА47-29 IEK предназначена для ...",
      "price": 5.00,
      "qty": 25,
      "categories": ["Категория 1", "Категория 2"],
      "image_path": "mva20d_ubv_3.png"
    }

    Обзор основных этапов импорта


    • регистрация самого продукта
    • связь продукта и web-сайта
    • базовые атрибуты продукта (EAV)
    • ивентарные данные (количество продукта на складе)
    • медиа (картинки)
    • связь с категориями каталога

    Регистрация продукта


    Базовая информация о продукте находится в catalog_product_entity:


    CREATE TABLE `catalog_product_entity` (
      `entity_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'Entity Id',
      `attribute_set_id` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT 'Attribute Set ID',
      `type_id` varchar(32) NOT NULL DEFAULT 'simple' COMMENT 'Type ID',
      `sku` varchar(64) DEFAULT NULL COMMENT 'SKU',
      `has_options` smallint(6) NOT NULL DEFAULT '0' COMMENT 'Has Options',
      `required_options` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT 'Required Options',
      `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation Time',
      `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update Time',
      PRIMARY KEY (`entity_id`),
      KEY `CATALOG_PRODUCT_ENTITY_ATTRIBUTE_SET_ID` (`attribute_set_id`),
      KEY `CATALOG_PRODUCT_ENTITY_SKU` (`sku`)
    )

    Минимально необходимая информация для создания записи в реестре продуктов:


    • attribute_set_id
    • sku

    дополнительная:


    • type_id — если не укажем, то будет использовано 'simple'

    Для прямой записи в базу использую DB-адаптер самой Magento:


    function create($sku, $typeId, $attrSetId)
    {
        /** @var \Magento\Framework\App\ResourceConnection $this->resource */
        /** @var \Magento\Framework\DB\Adapter\Pdo\Mysql $conn */
        $conn = $this->resource->getConnection();
        $table = $this->resource->getTableName('catalog_product_entity');
        $bind = [
            'sku' => $sku,
            'type_id' => $typeId,
            'attribute_set_id' => $attrSetId
        ];
        $conn->insert($table, $bind);
        $result = $conn->lastInsertId($table);
        return $result;
    }

    После регистрации продукта в catalog_product_entity он становится видимым в админке, в гриде продуктов (Catalog / Products).


    image


    Связь продукта и web-сайта


    Связь продукта с сайтом определяет, в каких магазинах и на каких витринах продукт будет доступен на фронте.


    function linkToWebsite($prodId, $websiteId)
    {
        /** @var \Magento\Framework\App\ResourceConnection $this->resource */
        /** @var \Magento\Framework\DB\Adapter\Pdo\Mysql $conn */
        $conn = $this->resource->getConnection();
        $table = $this->resource->getTableName('catalog_product_website');
        $bind = [
            'product_id' => $prodId,
            'website_id' => $websiteId
        ];
        $conn->insert($table, $bind);
    }

    image


    Базовые атрибуты продукта


    У свежезарегистрированного продукта пока нет ни имени, ни описания. Всё это делается через EAV-аттрибуты. Вот список базовых атрибутов продукта, которые нужны для того, чтобы продукт достаточно корректно показывался на фронте:


    • name
    • price
    • description
    • short_description
    • status
    • tax_class_id
    • url_key
    • visibility

    Отдельный атрибут к продукту добавляется вот так (опущены детали получения идентификатора и типа атрибута по его коду):


    public function create($prodId, $attrCode, $attrValue)
    {
        $attrId = /* get attribute ID by attribute code */
        $attrType = /* get attribute type [datetime|decimal|int|text|varchar]) by attribute code */
        if ($attrId) {
            /** @var \Magento\Framework\App\ResourceConnection $this->resource */
            /** @var \Magento\Framework\DB\Adapter\Pdo\Mysql $conn */
            $conn = $this->resource->getConnection();
            $tblName = 'catalog_product_entity_' . $attrType;
            $table = $this->resource->getTableName($tblName);
            $bind = [
                'attribute_id' => $attrId,
                'entity_id' => $prodId,
                /* put all attributes to default store view with id=0 (admin) */
                'store_id' => 0,
                'value' => $attrValue
            ];
            $conn->insert($table, $bind);
        }
    }

    По коду атрибута определяем его id и тип данных (datetime, decimal, int, text, varchar), затем в соответствующую таблицу пишем данные для административной витрины (store_id = 0).


    После добавления вышеперечисленных атрибутов к продукту получается вот такая картинка в админке:


    image


    Инвентарные данные


    Начиная с версии 2.3 в Magento параллельно существует два набора таблиц, обеспечивающих хранение инвентарной информации (количество продукта):


    • cataloginventory_*: старая структура;
    • inventory_*: новая структура (MSI — Multi Source Inventory);

    Добавлять инвентарные данные нужно в обе структуры, т.к. новая структура пока ещё не полностью независима от старой (очень похоже, что для default склада в новой структуре задействована таблица cataloginventory_stock_status в качестве inventory_stock_1).


    cataloginventory_


    При развёртывании Magneto 2.3 мы изначально имеем 2 записи в store_website, что соответствует двум сайтам — административному и основному клиентскому:


    website_id|code |name        |sort_order|default_group_id|is_default|
    ----------|-----|------------|----------|----------------|----------|
             0|admin|Admin       |         0|               0|         0|
             1|base |Main Website|         0|               1|         1|

    В таблице cataloginventory_stock у нас есть только одна запись:


    stock_id|website_id|stock_name|
    --------|----------|----------|
           1|         0|Default   |

    Т.е., у нас в старой структуре есть только один "склад" (stock) и он привязан к административному website'у. Добавление через админку новых sources/stocks в MSI (новую структуру) не приводит к появлению новых записей в cataloginventory_stock.


    Инвентарные данные о продуктах в старой структуре изначально прописываются в таблицах:


    • cataloginventory_stock_item
    • cataloginventory_stock_status

    cataloginventory_stock_item


    function createOldItem($prodId, $qty)
    {
        $isQtyDecimal = (((int)$qty) != $qty);
        $isInStock = ($qty > 0);
        /** @var \Magento\Framework\App\ResourceConnection $this->resource */
        /** @var \Magento\Framework\DB\Adapter\Pdo\Mysql $conn */
        $conn = $this->resource->getConnection();
        $table = $this->resource->getTableName('cataloginventory_stock_item');
        $bind = [
            'product_id' => $prodId,
            /* we use one only stock in 'cataloginventory' structure by default */
            'stock_id' => 1,
            'qty' => $qty,
            'is_qty_decimal' => $isQtyDecimal,
            'is_in_stock' => $isInStock,
            /* default stock is bound to admin website (see `cataloginventory_stock`) */
            'website_id' => 0
        ];
        $conn->insert($table, $bind);
    }

    cataloginventory_stock_status


    function createOldStatus($prodId, $qty)
    {
        $isInStock = ($qty > 0);
        /** @var \Magento\Framework\App\ResourceConnection $this->resource */
        /** @var \Magento\Framework\DB\Adapter\Pdo\Mysql $conn */
        $conn = $this->resource->getConnection();
        $table = $this->resource->getTableName('cataloginventory_stock_status');
        $bind = [
            'product_id' => $prodId,
            /* we use one only stock in 'cataloginventory' structure by default */
            'stock_id' => 1, 
            'qty' => $qty,
            'stock_status' => \Magento\CatalogInventory\Api\Data\StockStatusInterface::STATUS_IN_STOCK,
            /* default stock is bound to admin website (see `cataloginventory_stock`) */
            'website_id' => 0 
        ];
        $conn->insert($table, $bind);
    }

    inventory_


    Изначально новая структура для хранения инвентарных данных содержит 1 "источник" (inventory_source):


    source_code|name          |enabled|description   |latitude|longitude|country_id|...|
    -----------|--------------|-------|--------------|--------|---------|----------|...|
    default    |Default Source|      1|Default Source|0.000000| 0.000000|US        |...|

    и один "склад" (inventory_stock):


    stock_id|name         |
    --------|-------------|
           1|Default Stock|

    "Источник" представляет собой физическое хранилище для продуктов (запись содержит физические координаты и почтовый адрес). "Склад" представляет собой логическое объединение нескольких "источников" (inventory_source_stock_link)


    link_id|stock_id|source_code|priority|
    -------|--------|-----------|--------|
          1|       1|default    |       1|

    на уровне которого происходит привязка к каналу продажи (inventory_stock_sales_channel)


    type   |code|stock_id|
    -------|----|--------|
    website|base|       1|

    Судя по структуре данных предполагаются различные типы каналов продаж, но по-умолчанию используется только связь "stock"-"website" (ссылка на web-сайт идёт по коду web-сайта — base).


    Один "склад" может быть привязан к нескольким "источникам", а один "источник" — к нескольким "складам" (отношение "многие-ко-многим"). Исключения составляют default'овые "источник" и "склад". Они не перепривязываются к другим сущностям (ограничение на уровне кода — вылетает ошибка "Can not save link related to Default Source or Default Stock"). Более подробно о структуре MSI в Magento 2 можно прочитать в статье "Система управления складом с использованием CQRS и Event Sourcing. Проектирование".


    Я буду использовать default'овую конфигурацию и добавлять всю инвентарную информацию в источник default, который задействован в канале продажи, связанном с web-сайтом с кодом base (соответствует клиентской части магазина — см. store_website):


    function createNewItem($sku, $qty)
    {
        /** @var \Magento\Framework\App\ResourceConnection $this->resource */
        /** @var \Magento\Framework\DB\Adapter\Pdo\Mysql $conn */
        $conn = $this->resource->getConnection();
        $table = $this->resource->getTableName('inventory_source_item');
        $bind = [
            'source_code' => 'default',
            'sku' => $sku,
            'quantity' => $qty,
            'status' => \Magento\InventoryApi\Api\Data\SourceItemInterface::STATUS_IN_STOCK
        ];
        $conn->insert($table, $bind);
    }

    После добавления инвентарных данных к продукту в админке получается вот такая картинка:


    image


    Медиа


    При "ручном" добавлении к продукту изображения через админку соответствующая информация прописывается в следующих таблицах:


    • catalog_product_entity_media_gallery: медиа-реестр (изображения и видео-файлы);
    • catalog_product_entity_media_gallery_value: привязка медиа к продуктам и витринам (локализация);
    • catalog_product_entity_media_gallery_value_to_entity: привязка медиа только к продуктам (предположительно, default медиа-контент для продукта);
    • catalog_product_entity_varchar: здесь сохраняются роли, в которых используется изображение;

    а сами изображения сохраняются в каталог ./pub/media/catalog/product/x/y/, где x и y — первая и вторая буквы имени файла с изображением. Например, файл image.png должен быть сохранён как ./pub/media/catalog/product/i/m/image.png, чтобы платформа могла использовать его в качестве изображения при описании продуктов из каталога.



    Регистрируем размещённый в ./pub/media/catalog/product/ медиа-файл (сам процесс размещения файла в данной статье не рассматривается):


    function createMediaGallery($imgPathPrefixed)
    {
        $attrId = /* get attribute ID by attribute code 'media_gallery' */
        /** @var \Magento\Framework\App\ResourceConnection $this->resource */
        /** @var \Magento\Framework\DB\Adapter\Pdo\Mysql $conn */
        $conn = $this->resource->getConnection();
        $table = $this->resource->getTableName('catalog_product_entity_media_gallery');
        $bind = [
            'attribute_id' => $attrId,
            'value' => $imgPathPrefixed,
            /* 'image' or 'video' */
            'media_type' => 'image',
            'disabled' => false
        ];
        $conn->insert($table, $bind);
        $result = $conn->lastInsertId($table);
        return $result;
    }

    При регистрации новому медиа-файлу присваивается идентификтор.



    Связываем зарегистрированный медиа-файл с соответствующим продуктом для default-витрины:


    function createGalleryValue($mediaId, $prodId)
    {
        /** @var \Magento\Framework\App\ResourceConnection $this->resource */
        /** @var \Magento\Framework\DB\Adapter\Pdo\Mysql $conn */
        $conn = $this->resource->getConnection();
        $table = $this->resource->getTableName('catalog_product_entity_media_gallery_value');
        $bind = [
            'value_id' => $mediaId,
            /* use admin store view by default */
            'store_id' => 0,
            'entity_id' => $prodId,
            'label' => null,
            /* we have one only image */
            'position' => 1,
            'disabled' => false
        ];
        $conn->insert($table, $bind);
    }


    Связываем зарегистрированный медиа-файл с соответствующим продуктом без привязки к какой-либо витрине. Не понятно, где именно используются эти данные и почему нельзя обращаться к данным предыдущей таблицы, но вот эта таблица существует и данные в неё записываются при добавлении картинки к продукту. Поэтому вот так.


    function createGalleryValueToEntity($mediaId, $prodId)
    {
        /** @var \Magento\Framework\App\ResourceConnection $this->resource */
        /** @var \Magento\Framework\DB\Adapter\Pdo\Mysql $conn */
        $conn = $this->resource->getConnection();
        $table = $this->resource->getTableName('catalog_product_entity_media_gallery_value_to_entity');
        $bind = [
            'value_id' => $mediaId,
            'entity_id' => $prodId
        ];
        $conn->insert($table, $bind);
    }

    catalog_product_entity_varchar


    Медиа-файл может использоваться с разными ролями (в скобках указан код соответствующего атрибута):


    • Base (image)
    • Small Image (small_image)
    • Thumbnail (thumbnail)
    • Swatch Image (swatch_image)

    Привязка ролей к медиа-файлу как раз и происходит в catalog_product_entity_varchar. Код привязки аналогичен коду в разделе "Базовые атрибуты продукта".


    После добавления изображения к продукту в админке получается вот так:


    image


    Категории


    Основные таблицы, в которых содержатся данные по категориям:


    • catalog_category_entity: реестр категорий;
    • catalog_category_product: связь продуктов и категорий;
    • catalog_category_entity_*: значения EAV-атрибутов;

    Изначально, в пустом Magento-приложении в реестре категорий содержится 2 категории (я сократил названия колонок: crtcreated_at, updupdated_at):


    entity_id|attribute_set_id|parent_id|crt|upd|path|position|level|children_count|
    ---------|----------------|---------|---|---|----|--------|-----|--------------|
            1|               3|        0|...|...|1   |       0|    0|             1|
            2|               3|        1|...|...|1/2 |       1|    1|             0|

    Категория с id=1 является корнем всего Magento-каталога и недоступна ни в админке, ни на фронте. Категория с id=2 (Default Category) является корневой категорией для основного магазина основного сайта (Main Website Store), создаваемого при развёртывании приложения (см. Admin / Stores / All Stores). Причём сама корневая категория магазина на фронте также недоступна, только её подкатегории.


    Поскольку темой данной статьи всё-таки является импорт данных по продуктам, то я не буду использовать прямую запись в базу при создании категорий, а воспользуюсь классами предоставляемыми самой Magento (модели и репозитории). Прямая запись в базу используется только для связи импортируемого продукта с категорией (сопоставление категории происходит по её имени, при сопоставлении извлекается id категории):


    function create($prodId, $catId)
    {
        /** @var \Magento\Framework\App\ResourceConnection $this->resource */
        /** @var \Magento\Framework\DB\Adapter\Pdo\Mysql $conn */
        $conn = $this->resource->getConnection();
        $table = $this->resource->getTableName('catalog_category_product');
        $bind = [
            'category_id' => $catId,
            'product_id' => $prodId,
        ];
        $conn->insert($table, $bind);
    }

    После добавления связи продукта с категориями "Категория 1" и "Категория 2" детали продукта в админке выглядят примерно так:


    image


    Дополнительные действия


    После завершения импорта данных нужно выполнить следующие дополнительные действия:


    • индексация данных: вызов в консоли ./bin/magento indexer:reindex;
    • регенерация URL'ов для продуктов/категорий: можно использовать расширение "elgentos/regenerate-catalog-urls"

    Продукты в админке после выполнения дополнительных действий:


    image


    и на фронте:


    image


    Резюме


    Тот же самый набор продуктов (10 штук), что и в прошлой статье, импортируется, как минимум, на порядок быстрее (1 секунда против 10). Для более точной оценки скорости нужно большее количество продуктов — несколько сотен, а лучше тысяч. Тем не менее, даже при таком небольшом размере входных данных можно сделать вывод, что использование инструментария, предоставляемого Magento (модели и репозитории), значительно (акцентирую — значительно!) ускоряют разработку требуемого функционала, но при этом значительно (акцентирую — значительно!) снижают скорость попадания данных в базу.


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

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      0
      Зачем вы с помощью прямого обращения к таблицам делаете то, что прекрасно делает
      \Magento\Catalog\Model\Product\Gallery\CreateHandler
      за один вызов execute()?
      Хотя дочитал — вы думаете так быстрее, но я не понимаю почему это работает у вас так медленно, у меня 100 продуктов (по несколько фото у каждого) импортируются за пару секунд — не дольше.
      Вы пробовали отключать cron job magento, чтобы одновременно с импортом не шла индексация?
        0

        У меня на ноуте не настроен cron для Magento. А прямо в базу я пишу потому, что так действительно быстрее. Плюсом идёт, что я могу заливать данные в БД без использования кода Magento в принципе. Хоть sql-скриптами. Использование классов Magento, в том числе и \Magento\Catalog\Model\Product\Gallery\CreateHandler, я рассматривал в предыдущей статье.

          0
          Здесь скорее всего речь идёт про update on save VS by schedule. И то это не про ваш случай. Поскольку индексы триггерятся только при полноценном сохранении продукта. В случае с записью в базу должно только добавляться значение в ..._cl_1 таблицы для последующего точечного реиндекса.
          Для буста перформанса я бы порекомендовал формировать батчи и делать insertMultiple()

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое