V 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 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.

Kód, který vznikl jako výsledek napsání obou článků, lze zhlédnout v Magento modulu „“. 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_idsku
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).

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);
}
Základní atributy produktu
Nově registrovaný produkt zatím nemá žádné jméno ani popis. To vše se provádí prostřednictvím . Zde je seznam základních atributů produktu, které jsou nezbytné pro to, aby se produkt dostatečně správně zobrazoval na front-endu:
namepricedescriptionshort_descriptionstatustax_class_idurl_keyvisibility
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:

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_itemcataloginventory_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 „«.
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:

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.
catalog_product_entity_media_gallery
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.
catalog_product_entity_media_gallery_value
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);
}catalog_product_entity_media_gallery_value_to_entity
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:

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:

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í „«
Produkty v administraci po provedení dodatečných akcí:

a na frontendu:

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
