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) і ён прывязаны да адміністрацыйнага 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 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крыніца»І«склад“. Яны не перапрывязваюцца да іншых сутнасцяў (абмежаванне на ўзроўні кода - вылятае памылкаCan not save 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

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

  • Base (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