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

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

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

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

Код, що вийшов у результаті написання обох статей, можна переглянути в 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 MagentoFrameworkAppResourceConnection $this->resource */
    /** @var MagentoFrameworkDBAdapterPdoMysql $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).

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

Зв'язок продукту та web-сайту

Зв'язок продукту з сайтом визначає, в яких магазинах та на яких вітринах продукт буде доступний на фронті.

function linkToWebsite($prodId, $websiteId)
{
    /** @var MagentoFrameworkAppResourceConnection $this->resource */
    /** @var MagentoFrameworkDBAdapterPdoMysql $conn */
    $conn = $this->resource->getConnection();
    $table = $this->resource->getTableName('catalog_product_website');
    $bind = [
        'product_id' => $prodId,
        'website_id' => $websiteId
    ];
    $conn->insert($table, $bind);
}

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

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

У свіжозареєстрованого продукту поки що немає ні імені, ні опису. Все це робиться через 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 MagentoFrameworkAppResourceConnection $this->resource */
        /** @var MagentoFrameworkDBAdapterPdoMysql $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).

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

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

Інвентарні дані

Починаючи з версії 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) і він прив'язаний до адміністративного веб-сайту. Додавання через адмінку нових 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 MagentoFrameworkAppResourceConnection $this->resource */
    /** @var MagentoFrameworkDBAdapterPdoMysql $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 MagentoFrameworkAppResourceConnection $this->resource */
    /** @var MagentoFrameworkDBAdapterPdoMysql $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' => MagentoCatalogInventoryApiDataStockStatusInterface::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|

Судячи з структури даних передбачаються різні типи каналів продажів, але за умовчанням використовується лише зв'язокакції«-«сайт» (Посилання на web-сайт йде за кодом web-сайту - base).

Один "склад» може бути прив'язаний до кількох «джерелами", а один "джерело» — до кількох «складам»(Ставлення «багатьом-багатьом»). Винятки становлять default'ові.джерело» та «склад“. Вони не перев'язуються до інших сутностей (обмеження на рівні коду - вилітає помилка)Не можна використовувати link related to Default Source або Default Stock«). Докладніше про структуру MSI Magento 2 можна прочитати в статті «Система керування складом з використанням CQRS та Event Sourcing. Проектування".

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

function createNewItem($sku, $qty)
{
    /** @var MagentoFrameworkAppResourceConnection $this->resource */
    /** @var MagentoFrameworkDBAdapterPdoMysql $conn */
    $conn = $this->resource->getConnection();
    $table = $this->resource->getTableName('inventory_source_item');
    $bind = [
        'source_code' => 'default',
        'sku' => $sku,
        'quantity' => $qty,
        'status' => MagentoInventoryApiApiDataSourceItemInterface::STATUS_IN_STOCK
    ];
    $conn->insert($table, $bind);
}

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

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

Медіа

При «ручному» додаванні до продукту зображення через адмінку відповідна інформація прописується у таких таблицях:

  • 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 MagentoFrameworkAppResourceConnection $this->resource */
    /** @var MagentoFrameworkDBAdapterPdoMysql $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 MagentoFrameworkAppResourceConnection $this->resource */
    /** @var MagentoFrameworkDBAdapterPdoMysql $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 MagentoFrameworkAppResourceConnection $this->resource */
    /** @var MagentoFrameworkDBAdapterPdoMysql $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

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

  • База (image)
  • Small Image (small_image)
  • Thumbnail (thumbnail)
  • Swatch Image (swatch_image)

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

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

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

Категорії

Основні таблиці, в яких містяться дані за категоріями:

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

Спочатку, в порожньому Magento-додатку в реєстрі категорій міститься 2 категорії (я скоротив назви колонок: crt - created_at, upd - updated_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 (Категорія за замовчуванням) є кореневою категорією для основного магазину основного сайту (Main Website Store), створюваного при розгортанні програми (див. Admin / Stores / All Stores). Причому сама коренева категорія магазину на фронті також недоступна лише її підкатегорії.

Оскільки темою даної статті все-таки є імпорт даних по продуктам, то я не використовуватиму прямий запис в базу при створенні категорій, а скористаюся класами, що надаються Magento (моделі та репозиторії). Прямий запис в базу використовується тільки для зв'язку імпортованого продукту з категорією (порівняння категорії відбувається за її ім'ям, при зіставленні вилучається id категорії):

function create($prodId, $catId)
{
    /** @var MagentoFrameworkAppResourceConnection $this->resource */
    /** @var MagentoFrameworkDBAdapterPdoMysql $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" деталі продукту в адмінці виглядають приблизно так:

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

додаткові дії

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

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

Продукти в адмінці після виконання додаткових дій:

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

та на фронті:

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

Резюме

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

У результаті вода виявилася мокрою і це не одкровення. Тим не менш, тепер я маю код, щоб грати далі і, можливо, зробити більш цікаві висновки.

Джерело: habr.com