В попередній статті я описав процес імпорту продуктів у Magento 2 звичайним способом – через моделі та репозиторії. Звичайний спосіб відрізняється дуже низькою швидкістю обробки даних. На моєму ноутбуці виходило приблизно один продукт на секунду. У цьому продовженні я розглядаю альтернативний спосіб імпорту продукту — прямий запис у базу, в обхід стандартних механізмів Magento 2 (моделі, фабрики, репозиторії). Послідовність кроків, що забезпечують імпорт продуктів, може бути адаптована під будь-яку мову програмування, здатну працювати з MySQL.
відмова: У Magento є готовий функціонал по імпорту даних і швидше за все, вам його вистачить. Однак якщо вам потрібен повний контроль за процесом імпорту, який не обмежується підготовкою CSV-файлу для того, що є, ласкаво просимо під кат.
Код, що вийшов у результаті написання обох статей, можна переглянути в 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:
У свіжозареєстрованого продукту поки що немає ні імені, ні опису. Все це робиться через 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).
Після додавання вищеперелічених атрибутів до продукту виходить така картинка в адмінці:
Інвентарні дані
Починаючи з версії 2.3 у Magento паралельно існує два набори таблиць, що забезпечують зберігання інвентарної інформації (кількість продукту):
cataloginventory_*: Стара структура;
inventory_*: нова структура (MSI - Multi Source Inventory);
Додавати інвентарні дані необхідно обидві структури, т.к. нова структура поки що не повністю незалежна від старої (дуже схоже, що для default складу у новій структурі задіяна таблиця cataloginventory_stock_status як inventory_stock_1).
cataloginventory_
При розгортанні Magneto 2.3 ми спочатку маємо 2 записи store_website, що відповідає двом сайтам - адміністративному та основному клієнтському:
Тобто, у нас у старій структурі є лише один «склад» (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):
«Джерело» являє собою фізичне сховище для продуктів (запис містить фізичні координати та поштову адресу). «Склад» являє собою логічне об'єднання кількох «джерел» (inventory_source_stock_link)
на рівні якого відбувається прив'язка до каналу продажу (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):
Після додавання інвентарних даних до продукту в адмінці виходить така картинка:
Медіа
При «ручному» додаванні до продукту зображення через адмінку відповідна інформація прописується у таких таблицях:
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щоб платформа могла використовувати його як зображення при описі продуктів з каталогу.
catalog_product_entity_media_gallery
Реєструємо розміщений у ./pub/media/catalog/product/ медіа-файл (сам процес розміщення файлу в цій статті не розглядається):
Зв'язуємо зареєстрований медіа-файл із відповідним продуктом без прив'язки до будь-якої вітрини. Не зрозуміло, де саме використовуються ці дані і чому не можна звертатися до даних попередньої таблиці, але ця таблиця існує і дані до неї записуються при додаванні картинки до продукту. Тож ось так.
Медіа-файл може використовуватись з різними ролями (у дужках вказаний код відповідного атрибуту):
База (image)
Small Image (small_image)
Thumbnail (thumbnail)
Swatch Image (swatch_image)
Прив'язка ролей до медіа-файлу якраз і відбувається в catalog_product_entity_varchar. Код прив'язки аналогічний коду у розділі «Базові атрибути продукту".
Після додавання зображення до продукту в адмінці виходить так:
Категорії
Основні таблиці, в яких містяться дані за категоріями:
catalog_category_entity: реєстр категорій;
catalog_category_product: зв'язок продуктів та категорій;
catalog_category_entity_*: значення EAV-атрибутів;
Спочатку, в порожньому Magento-додатку в реєстрі категорій міститься 2 категорії (я скоротив назви колонок: crt - created_at, upd - updated_at):
Категорія з id=1 є коренем всього Magento-каталогу та недоступна ні в адмінці, ні на фронті. Категорія з id=2 (Категорія за замовчуванням) є кореневою категорією для основного магазину основного сайту (Main Website Store), створюваного при розгортанні програми (див. Admin / Stores / All Stores). Причому сама коренева категорія магазину на фронті також недоступна лише її підкатегорії.
Оскільки темою даної статті все-таки є імпорт даних по продуктам, то я не використовуватиму прямий запис в базу при створенні категорій, а скористаюся класами, що надаються Magento (моделі та репозиторії). Прямий запис в базу використовується тільки для зв'язку імпортованого продукту з категорією (порівняння категорії відбувається за її ім'ям, при зіставленні вилучається id категорії):
Продукти в адмінці після виконання додаткових дій:
та на фронті:
Резюме
Той самий набір продуктів (10 штук), що й у минулій статті, імпортується як мінімум на порядок швидше (1 секунда проти 10). Для більш точної оцінки швидкості потрібна більша кількість продуктів – кілька сотень, а краще тисяч. Тим не менш, навіть при такому невеликому розмірі вхідних даних можна зробити висновок, що використання інструментарію, що надається Magento (моделі та репозиторії), значно (акцентую - значно!) прискорюють розробку необхідного функціоналу, але при цьому значно (акцентую - значно!) знижують швидкість попадання даних до бази.
У результаті вода виявилася мокрою і це не одкровення. Тим не менш, тепер я маю код, щоб грати далі і, можливо, зробити більш цікаві висновки.