Magento 2: import produktů přímo do databáze

V předchozí článku Popisoval jsem proces importu produktů do Magento 2 obvyklým způsobem – prostřednictvím modelů a repozitářů. Obvyklý způsob se vyznačuje velmi nízkou rychlostí zpracování dat. Na mém notebooku to vycházelo přibližně na jeden produkt za sekundu. V tomto pokračování zkoumám alternativní způsob importu produktu – přímým zápisem do databáze, obcházejícím standardní mechanismy Magento 2 (modely, továrny, repozitáře). Sekvence kroků umožňujících import produktů může být přizpůsobena jakémukoli programovacímu jazyku, který dokáže pracovat s MySQL.

Poznámka: Magento má připravenou funkčnost pro import dat a pravděpodobně vám ji bude stačit. Pokud však potřebujete větší kontrolu nad procesem importu, která neomezuje jen na přípravu CSV souboru pro to, co existuje – vítejte pod kat.

Magento 2: import produktů přímo do databáze

Kód, který vznikl jako výsledek napsání obou článků, lze zhlédnout v Magento modulu „flancer32/mage2_ext_demo_import“. Zde jsou některá omezení, která jsem dodržoval, abych zjednodušil kód demo modulu:

  • Produkty jsou pouze vytvářeny, neaktualizovány.
  • Jeden sklad
  • Importovány jsou pouze názvy kategorií, bez jejich struktury
  • Datové struktury odpovídají verzi 2.3

JSON pro import jednotlivého produktu:

{
  "sku": "MVA20D-UBV-3",
  "name": "Zátka pro plombování VA47-29 IEK",
  "desc": "Zajištění přístupu k zařízením ...",
  "desc_short": "Zátka pro plombování VA47-29 IEK je určena pro ...",
  "price": 5.00,
  "qty": 25,
  "categories": ["Kategorie 1", "Kategorie 2"],
  "image_path": "mva20d_ubv_3.png"
}

Přehled hlavních kroků importu

  • registrace samotného produktu
  • spojení produktu a webu
  • základní atributy produktu (EAV)
  • inventární údaje (množství produktu na skladu)
  • média (obrázky)
  • spojení s kategoriemi katalogu

Registrace produktu

Základní informace o produktu se nachází v 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`)
)

Minimálně potřebné informace pro vytvoření záznamu v registru produktů:

  • attribute_set_id
  • sku

další:

  • type_id — pokud neudáme, použije se 'simple'

Pro přímý zápis do databáze používám DB adaptér samotného 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;
}

Po registraci produktu v catalog_product_entity se stane viditelným v administraci, v gridu produktů (Catalog / Products).

Magento 2: import produktů přímo do databáze

Propojování produktu a webové stránky

Propojení produktu s webem určuje, ve kterých obchodech a na kterých výlohách bude produkt dostupný na front-endu.

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: import produktů přímo do databáze

Základní atributy produktu

Nově registrovaný produkt zatím nemá žádné jméno ani popis. To vše se provádí prostřednictvím EAV atributů. Zde je seznam základních atributů produktu, které jsou nezbytné pro to, aby se produkt dostatečně správně zobrazoval na front-endu:

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

Samostatný atribut k produktu se přidává takto (detaily o získání identifikátoru a typu atributu podle jeho kódu jsou vynechány):

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);
    }
}

Podle kódu atributu určujeme jeho id a typ dat (datetime, decimal, int, text, varchar), poté do příslušné tabulky zapisujeme data pro administrační výlohu (store_id = 0).

Po přidání výše uvedených atributů k produktu vypadá administrativní rozhraní takto:

Magento 2: import produktů přímo do databáze

Inventární data

Od verze 2.3 v Magento existují paralelně dva sady tabulek, které zajišťují uchovávání inventárních informací (množství produktu):

  • cataloginventory_*: stará struktura;
  • inventory_*: nová struktura (MSI — Multi Source Inventory);

Musíte přidávat inventární data do obou struktur, protože nová struktura zatím není zcela nezávislá na staré (velmi pravděpodobně se pro default sklad v nové struktuře používá tabulka cataloginventory_stock_status jako inventory_stock_1).

cataloginventory_

Při nasazení Magneto 2.3 máme na začátku 2 záznamy v store_website, což odpovídá dvěma webům — administrativnímu a hlavnímu klientskému:

website_id|code |name        |sort_order|default_group_id|is_default|
----------|-----|------------|----------|----------------|----------|
         0|admin|Admin       |         0|               0|         0|
         1|base |Hlavní web  |         0|               1|         1|

V tabulce cataloginventory_stock máme pouze jeden záznam:

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

To znamená, že máme ve staré struktuře pouze jeden „sklad“ (stock) a ten je spojen s administrativním webem. Přidání nových sources/stocks do MSI (nové struktury) nevede k objevení nových záznamů v cataloginventory_stock.

Inventární data o produktech ve staré struktuře jsou původně uchovávána v tabulkách:

  • 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,
        /* používáme pouze jeden sklad ve struktuře 'cataloginventory' jako výchozí */
        'stock_id' => 1,
        'qty' => $qty,
        'is_qty_decimal' => $isQtyDecimal,
        'is_in_stock' => $isInStock,
        /* výchozí sklad je připojen k administrativnímu webu (viz `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,
        /* používáme pouze jeden sklad ve struktuře 'cataloginventory' jako výchozí */
        'stock_id' => 1, 
        'qty' => $qty,
        'stock_status' => MagentoCatalogInventoryApiDataStockStatusInterface::STATUS_IN_STOCK,
        /* výchozí sklad je připojen k administrativnímu webu (viz `cataloginventory_stock`) */
        'website_id' => 0 
    ];
    $conn->insert($table, $bind);
}

inventory_

Původně nová struktura pro uchovávání inventárních dat obsahuje 1 „zdroj» (inventory_source):

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

a jeden „sklad» (inventory_stock):

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

«Zdroj“ představuje fyzické úložiště pro produkty (záznam obsahuje fyzické souřadnice a poštovní adresu). „Sklad“ představuje logické spojení několika „zdrojů“ (inventory_source_stock_link)

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

na úrovni, na které dochází k propojování s prodejním kanálem (inventory_stock_sales_channel)

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

Soudě podle struktury dat se předpokládají různé typy prodejních kanálů, ale ve výchozím nastavení se používá pouze spojení „stock«-«website“ (odkaz na webovou stránku se provádí podle kódu webové stránky — base).

Jeden „sklad“ může být propojen s několika „zdroji“, a jeden „zdroj“ — s několika „sklady“ (vztah „mnoho k mnoha“). Výjimkami jsou výchozí „zdroj“ a „sklad“. Tyto se nepřipojují k jiným entitám (omezení na úrovni kódu — dojde k chybě „Can not save link related to Default Source or Default Stock“). Podrobnosti o struktuře MSI v Magento 2 lze nalézt v článku „Správa skladu pomocí CQRS a Event Sourcing. Návrh«.

Budu používat výchozí konfiguraci a přidávat všechny informace o inventáři do zdroje default, který je zapojen do prodejního kanálu spojeného s webovou stránkou s kódem base (odpovídá klientské části obchodu — viz. 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);
}

Po přidání inventárních dat k produktu v administraci vzniká takovýto obrázek:

Magento 2: import produktů přímo do databáze

Média

Při „ručním“ přidání obrázků k produktu přes administraci se odpovídající informace zapisují do následujících tabulek:

  • catalog_product_entity_media_gallery: media registr (obrázky a videa);
  • catalog_product_entity_media_gallery_value: propojení média k produktům a výkladům (lokalizace);
  • catalog_product_entity_media_gallery_value_to_entity: propojení médií pouze k produktům (předpokládá se, že jde o výchozí mediální obsah pro produkt);
  • catalog_product_entity_varchar: zde jsou uloženy role, ve kterých se obrázek používá;

a samotné obrázky se ukládají v katalogu .\/pub\/media\/catalog\/product\/x\/y\/, kde x a y — první a druhé písmeno názvu souboru s obrázkem. Například, soubor image.png musí být uložen jako ./pub/media/catalog/product/i/m/image.png, aby platforma mohla použít obrázek při popisu produktů z katalogu.

Registrujeme uložený v .\/pub\/media\/catalog\/product\/ mediální soubor (samotný proces umístění souboru v tomto článku není zvažován):

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;
}

Při registraci novému mediálnímu souboru je přidělen identifikátor.

Propojujeme registrovaný mediální soubor s příslušným produktem pro defaultní vitRínu:

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,
        \/\* máme pouze jeden obrázek \/\*
        'position' => 1,
        'disabled' => false
    ];
    $conn->insert($table, $bind);
}

Propojujeme registrovaný mediální soubor s příslušným produktem bez vazby na jakoukoli vitRínu. Není jasné, kde přesně se tato data používají a proč nemůže být odkazováno na data předchozí tabulky, ale tato tabulka existuje a při přidání obrázku k produktu se do ní zapisují data. Proto to takto.

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

Mediální soubor může být používán s různými rolemi (v závorkách je uveden kód příslušného atributu):

  • Base (obrázek)
  • Small Image (small_image)
  • Thumbnail (thumbnail)
  • Swatch Image (swatch_image)

Přiřazení rolí k mediálnímu souboru se právě provádí v catalog_product_entity_varchar. Kód přiřazení je podobný kódu v sekci „Základní atributy produktu«.

Po přidání obrázku k produktu v administračním rozhraní to vypadá takto:

Magento 2: import produktů přímo do databáze

Kategorie

Hlavní tabulky, ve kterých jsou uložena data o kategoriích:

  • catalog_category_entity: registr kategorií;
  • catalog_category_product: vazba produktů a kategorií;
  • catalog_category_entity_*: hodnoty EAV atributů;

Původně, v prázdné aplikaci Magento, obsahuje registr kategorií 2 kategorie (zkrátil jsem názvy sloupců: 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|

Kategorie s id=1 je kořenem celého Magento katalogu a není dostupná ani v administraci, ani na frontendu. Kategorie s id=2 (Default Category) je kořenová kategorie pro hlavní obchod hlavního webu (Main Website Store), která se vytváří při nasazení aplikace (viz. Admin / Stores / All Stores). Přičemž sama kořenová kategorie obchodu není na frontendu dostupná, pouze její podkategorie.

Jelikož je téma tohoto článku především import dat o produktech, nebudu používat přímý zápis do databáze při vytváření kategorií, ale využiji třídy poskytované samotným Magento (modely a repozitáře). Přímý zápis do databáze je používán pouze pro propojení importovaného produktu s kategorií (propojení kategorie probíhá podle jejího názvu, při propojení se získává id kategorie):

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);
}

Po přidání vazby produktu s kategoriemi „Kategorie 1“ a „Kategorie 2“ detaily produktu v administraci vypadají přibližně takto:

Magento 2: import produktů přímo do databáze

Dodatečné akce

Po dokončení importu dat je třeba provést následující dodatečné akce:

  • indexace dat: vyvolání v konzoli ./bin/magento indexer:reindex;
  • regenerace URL pro produkty/kategorie: lze použít rozšíření „elgentos/regenerate-catalog-urls«

Produkty v administraci po provedení dodatečných akcí:

Magento 2: import produktů přímo do databáze

a na frontendu:

Magento 2: import produktů přímo do databáze

Shrnutí

Stejná sada produktů (10 kusů), jaká byla v minulém článku, se importuje minimálně o jeden řád rychleji (1 sekunda oproti 10). Pro přesnější vyhodnocení rychlosti je potřeba větší množství produktů — několik set, lépe však tisíce. Nicméně, i při tak malém objemu vstupních dat lze usoudit, že využití nástrojů poskytovaných Magento (modely a repozitáře) výrazně (zdůrazňuji — významně!) urychluje vývoj potřebné funkčnosti, ale zároveň výrazně (zdůrazňuji — významně!) snižuje rychlost přenosu dat do databáze.

Nakonec se ukázalo, že voda je mokrá a to rozhodně není žádné odhalení. Nicméně, nyní mám kód, se kterým mohu dále experimentovat a možná dospět k zajímavějším závěrům.

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster