W Opisałem tam proces importu produktów do Magento 2 w tradycyjny sposób — za pomocą modeli i repozytoriów. Tradycyjna metoda charakteryzuje się bardzo niską wydajnością przetwarzania danych. Na moim laptopie udawało się zaimportować około jednego produktu na sekundę. W tym artykule rozważam alternatywny sposób importu produktów — bezpośrednie wprowadzanie danych do bazy, omijając standardowe mechanizmy Magento 2 (modele, fabryki, repozytoria). Sekwencja kroków umożliwiających import produktów może być dostosowana do dowolnego języka programowania, który potrafi współpracować z MySQL.
Zastrzeżenie: W Magento istnieje gotowa funkcjonalność do i prawdopodobnie będzie ona wystarczająca. Jeśli jednak potrzebujesz większej kontroli nad procesem importu, nie ograniczającej się do przygotowania pliku CSV, zapraszam do dalszej lektury.

Kod utworzony w wyniku napisania obu artykułów można zobaczyć w module Magento „“. Oto niektóre ograniczenia, których się trzymałem, aby uprościć kod demo modułu:
- Produktu można tylko utworzyć, nie zaktualizować.
- Jedna magazyn
- Importowane są tylko nazwy kategorii, bez ich struktury
- Struktury danych odpowiadają wersji 2.3
JSON do importu pojedynczego produktu:
{
"sku": "MVA20D-UBV-3",
"name": "Zaślepka do plombowania VA47-29 IEK",
"desc": "Umożliwienie dostępu do urządzeń ...",
"desc_short": "Zaślepka do plombowania VA47-29 IEK jest przeznaczona do ...",
"price": 5.00,
"qty": 25,
"categories": ["Kategoria 1", "Kategoria 2"],
"image_path": "mva20d_ubv_3.png"
}Przegląd głównych etapów importu
- rejestracja samego produktu
- powiązanie produktu z witryną internetową
- podstawowe atrybuty produktu (EAV)
- dane inwentaryzacyjne (ilość produktu na stanie)
- media (zdjęcia)
- powiązanie z kategoriami katalogu
Rejestracja produktu
Podstawowe informacje o produkcie znajdują się w catalog_product_entity:
UTWÓRZ TABELĘ `catalog_product_entity` (
`entity_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'Id encji',
`attribute_set_id` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT 'Id zestawu atrybutów',
`type_id` varchar(32) NOT NULL DEFAULT 'simple' COMMENT 'Id typu',
`sku` varchar(64) DEFAULT NULL COMMENT 'SKU',
`has_options` smallint(6) NOT NULL DEFAULT '0' COMMENT 'Ma opcje',
`required_options` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT 'Wymagane opcje',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Czas utworzenia',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Czas aktualizacji',
PRIMARY KEY (`entity_id`),
KEY `CATALOG_PRODUCT_ENTITY_ATTRIBUTE_SET_ID` (`attribute_set_id`),
KEY `CATALOG_PRODUCT_ENTITY_SKU` (`sku`)
)Minimalne informacje potrzebne do stworzenia rekordu w rejestrze produktów:
attribute_set_idsku
dodatkowe:
type_id— jeśli nie zostanie podane, zostanie użyte ‘simple’
Do bezpośredniego zapisu w bazie używam adaptera DB samej 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 zarejestrowaniu produktu w catalog_product_entity staje się widoczny w panelu administracyjnym, w siatce produktów (Katalog / Produkty).

Powiązanie produktu z witryną internetową
Powiązanie produktu z witryną określa, w których sklepach i na jakich witrynach produkt będzie dostępny na froncie.
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);
}
Podstawowe atrybuty produktu
Nowo zarejestrowany produkt nie ma jeszcze ani nazwy, ani opisu. Wszystko to realizowane jest przez . Oto lista podstawowych atrybutów produktu, które są potrzebne, aby produkt był odpowiednio wyświetlany na froncie:
namepricedescriptionshort_descriptionstatustax_class_idurl_keyvisibility
Oddzielny atrybut do produktu dodaje się w ten sposób (pominięto szczegóły dotyczące uzyskania identyfikatora i typu atrybutu na podstawie jego kodu):
public function create($prodId, $attrCode, $attrValue)
{
$attrId = /* pobierz ID atrybutu na podstawie kodu atrybutu */
$attrType = /* pobierz typ atrybutu [datetime|decimal|int|text|varchar] na podstawie kodu atrybutu */
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,
/* umieść wszystkie atrybuty w domyślnej widoku sklepu z id=0 (admin) */
'store_id' => 0,
'value' => $attrValue
];
$conn->insert($table, $bind);
}
}Na podstawie kodu atrybutu określamy jego id oraz typ danych (datetime, decimal, int, text, varchar), następnie do odpowiedniej tabeli zapisujemy dane dla administracyjnej witryny (store_id = 0).
Po dodaniu powyższych atrybutów do produktu otrzymujemy następujący obraz w panelu administracyjnym:

Dane inwentarzowe
Począwszy od wersji 2.3 w Magento równolegle istnieją dwa zestawy tabel, które zapewniają przechowywanie informacji o inwentarzu (ilość produktu):
cataloginventory_*: stara struktura;inventory_*: nowa struktura (MSI — Multi Source Inventory);
Dane inwentarzowe należy dodawać do obu struktur, ponieważ nowa struktura wciąż nie jest całkowicie niezależna od starej (bardzo podobnie jak dla default magazyn w nowej strukturze wykorzystuje tabelę cataloginventory_stock_status jako inventory_stock_1).
cataloginventory_
Podczas wdrażania Magneto 2.3 początkowo mamy 2 rekordy w store_website, co odpowiada dwóm stronom — administracyjnej i głównej klienckiej:
website_id|code |name |sort_order|default_group_id|is_default|
----------|-----|------------|----------|----------------|----------|
0|admin|Admin | 0| 0| 0|
1|base |Główna strona| 0| 1| 1|W tabeli cataloginventory_stock mamy tylko jeden rekord:
stock_id|website_id|stock_name|
--------|----------|----------|
1| 0|Domyślny |Tzn., w starej strukturze mamy tylko jeden „magazyn” (stock) i jest powiązany z administracyjną stroną. Dodawanie przez panel administracyjny nowych sources/stocks w MSI (nowa struktura) nie prowadzi do pojawienia się nowych rekordów w cataloginventory_stock.
Dane inwentaryzacyjne produktów w starej strukturze są pierwotnie zapisywane w tabelach:
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,
/* 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_
Początkowa nowa struktura do przechowywania danych inwentarza zawiera 1 „źródło» (inventory_source):
source_code|name |enabled|description |latitude|longitude|country_id|...|
-----------|--------------|-------|--------------|--------|---------|----------|...|
default |Domyślny Źródło| 1|Domyślny Źródło|0.000000| 0.000000|US |...|i jeden «magazyn» (inventory_stock):
stock_id|name |
--------|-------------|
1|Domyślny Magazyn|«Źródło» jest fizycznym miejscem przechowywania produktów ( wpis zawiera fizyczne współrzędne i adres pocztowy). «Magazyn» jest logicznym połączeniem kilku «źródeł» (inventory_source_stock_link)
link_id|stock_id|source_code|priority|
-------|--------|-----------|--------|
1| 1|default | 1|na poziomie, na którym następuje powiązanie z kanałem sprzedaży (inventory_stock_sales_channel)
type |code|stock_id|
-------|----|--------|
website|base| 1|Z danych wynika, że przewidziano różne typy kanałów sprzedaży, ale domyślnie używany jest tylko związek «stock«-«website» (link do strony internetowej opiera się na kodzie strony internetowej — base).
Jeden «magazyn» może być powiązany z wieloma «źródłami«, a jeden «źródło» — z wieloma «magazynami» (relacja «wielu do wielu»). Wyjątkiem są domyślne «źródło» i «magazyn«. Nie można ich powiązać z innymi encjami (ograniczenie na poziomie kodu — występuje błąd «Nie można zapisać linku powiązanego z Źródłem Domyślnym lub Zapasem DomyślnymWięcej informacji o strukturze MSI w Magento 2 można znaleźć w artykule „«.
Będę korzystać z domyślnej konfiguracji i dodawać wszystkie informacje o inwentarzu do źródła default, które jest zaangażowane w kanał sprzedaży związany z witryną internetową z kodem base (odpowiada części klienckiej sklepu — patrz 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 dodaniu danych inwentarzowych do produktu w panelu administracyjnym otrzymujemy taki obrazek:

Media
Przy „ręcznym” dodawaniu obrazu do produktu przez panel administracyjny odpowiednie informacje zapisują się w następujących tabelach:
catalog_product_entity_media_gallery: rejestr mediów (obrazy i pliki wideo);catalog_product_entity_media_gallery_value: powiązanie mediów z produktami i witrynami (lokalizacja);catalog_product_entity_media_gallery_value_to_entity: przypisanie mediów tylko do produktów (przypuszczalnie, domyślna zawartość multimedialna dla produktu);catalog_product_entity_varchar: tutaj przechowywane są role, w których używane jest zdjęcie;
a same obrazy są przechowywane w katalogu ./pub/media/catalog/product/x/y/, gdzie x и y — pierwsza i druga litera nazwy pliku z obrazem. Na przykład, plik image.png powinien być zapisany jako ./pub/media/catalog/product/i/m/image.png, aby platforma mogła używać go jako obrazu podczas opisu produktów z katalogu.
catalog_product_entity_media_gallery
Rejestrujemy umieszczony w ./pub/media/catalog/product/ plik multimedialny (sam proces umieszczania pliku w niniejszym artykule nie jest omawiany):
function createMediaGallery($imgPathPrefixed)
{
$attrId = /* pobierz identyfikator atrybutu według kodu atrybutu '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' lub 'video' */
'media_type' => 'image',
'disabled' => false
];
$conn->insert($table, $bind);
$result = $conn->lastInsertId($table);
return $result;
}Przy rejestracji nowemu plikowi multimedialnemu przypisywany jest identyfikator.
catalog_product_entity_media_gallery_value
Łączymy zarejestrowany plik multimedialny z odpowiednim produktem dla domyślnej witryny:
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);
}catalog_product_entity_media_gallery_value_to_entity
Łączymy zarejestrowany plik multimedialny z odpowiednim produktem bez powiązania z żadnym sklepem. Nie jest jasne, gdzie są używane te dane ani dlaczego nie można odwołać się do danych z poprzedniej tabeli, ale ta tabela istnieje i dane są do niej zapisywane przy dodawaniu zdjęcia do produktu. Dlatego tak.
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
Plik multimedialny może być używany z różnymi rolami (w nawiasach podany jest kod odpowiadającego atrybutu):
- Podstawowy (
obraz) - Mały obraz (
mały_obraz) - Miniaturka (
miniaturka) - Obraz próbki (
obraz_próbki)
Powiązanie ról z plikiem multimedialnym odbywa się w catalog_product_entity_varchar. Kod powiązania jest analogiczny do kodu w sekcji „Podstawowe atrybuty produktu«.
Po dodaniu obrazu do produktu w panelu administracyjnym wygląda to tak:

Kategorie
Podstawowe tabele, w których znajdują się dane dotyczące kategorii:
catalog_category_entity: rejestr kategorii;catalog_category_product: powiązanie produktów i kategorii;catalog_category_entity_*: wartości atrybutów EAV;
Początkowo, w pustej aplikacji Magento w rejestrze kategorii znajdują się 2 kategorie (skróciłem nazwy kolumn: 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|Kategoria o id=1 jest korzeniem całego katalogu Magento i nie jest dostępna ani w panelu administracyjnym, ani na froncie. Kategoria o id=2 (Default Category) jest kategorią główną dla właściwego sklepu głównego (Main Website Store), tworzonego podczas wdrażania aplikacji (patrz. Admin / Stores / All Stores). Ponadto sama główna kategoria sklepu na froncie również nie jest dostępna, tylko jej podkategorie.
Ponieważ temat tego artykułu dotyczy importu danych o produktach, nie będę korzystać z bezpośredniego zapisu do bazy podczas tworzenia kategorii, a zamiast tego skorzystam z klas udostępnianych przez Magento (modele i repozytoria). Bezpośredni zapis do bazy służy tylko do powiązania importowanego produktu z kategorią (dopasowanie kategorii następuje na podstawie jej nazwy, a przy dopasowaniu wyciągane jest id kategorii):
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 dodaniu powiązania produktu z kategoriami „Kategoria 1” i „Kategoria 2” szczegóły produktu w panelu administracyjnym wyglądają mniej więcej tak:

Dodatkowe czynności
Po zakończeniu importu danych należy wykonać następujące dodatkowe czynności:
- indeksacja danych: wywołanie w konsoli
./bin/magento indexer:reindex; - regeneracja URL-i dla produktów/kategorii: można skorzystać z rozszerzenia „«
Produkty w panelu administracyjnym po wykonaniu dodatkowych czynności:

i na froncie:

Podsumowanie
Ten sam zestaw produktów (10 sztuk), co w poprzednim artykule, importuje się przynajmniej dziesięciokrotnie szybciej (1 sekunda zamiast 10). Aby dokładniej ocenić szybkość, potrzebna jest większa liczba produktów — kilkaset, a najlepiej tysiąc. Niemniej jednak, nawet przy tak małej ilości danych wejściowych można stwierdzić, że korzystanie z narzędzi oferowanych przez Magento (modele i repozytoria) znacząco (podkreślam — znacząco!) przyspiesza rozwój wymaganego funkcjonalności, jednocześnie znacznie (podkreślam — znacząco!) obniża szybkość wprowadzania danych do bazy.
Ostatecznie woda okazała się mokra i to żadna nowość. Niemniej jednak teraz mam kod, aby dalej eksperymentować i być może wyciągnąć ciekawsze wnioski.
Źródło: habr.com
