W Opisałem proces importu produktów do Magento 2 w standardowy sposób — za pomocą modeli i repozytoriów. Standardowa metoda charakteryzuje się dość niską szybkością przetwarzania danych. Na moim laptopie wychodziło około jednego produktu na sekundę. W tym kontynuacji rozważam alternatywny sposób importu produktu — bezpośrednim zapisem do bazy, omijając standardowe mechanizmy Magento 2 (modele, fabryki, repozytoria). Sekwencja kroków zapewniających import produktów może być dostosowana do dowolnego języka programowania, który jest w stanie pracować z MySQL.
Zrzeczenie się odpowiedzialności: W Magento istnieje gotowa funkcjonalność do i prawdopodobnie będzie wystarczająca. Jednak jeśli potrzebujesz pełniejszej kontroli nad procesem importu, nie ograniczającej się tylko do przygotowania pliku CSV — zapraszam do lektury.

Kod, który powstał w wyniku pisania obu artykułów, można zobaczyć w module Magento „“. Oto niektóre ograniczenia, których się trzymałem, aby uprościć kod modułu demo:
- Produkty są tylko tworzone, nie aktualizowane.
- Jedna lokalizacja
- Importowane są tylko nazwy kategorii, bez ich struktury.
- Struktury danych odpowiadają wersji 2.3.
JSON do importu pojedynczego produktu:
{
"sku": "MVA20D-UBV-3",
"name": "Zatyczka do plombowania VA47-29 IEK",
"desc": "Zapewnienie dostępu do urządzeń ...",
"desc_short": "Zatyczka 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
- połączenie produktu z witryną internetową
- podstawowe atrybuty produktu (EAV)
- dane inwentaryzacyjne (ilość produktu na magazynie)
- media (zdjęcia)
- połączenie z kategoriami katalogu
Rejestracja produktu
Podstawowe informacje o produkcie znajdują się w catalog_product_entity:
CREATE TABLE `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 niezbędne informacje do utworzenia wpisu w rejestrze produktów:
attribute_set_idsku
dodatkowe:
type_id— jeśli nie podamy, użyty zostanie 'simple'
Do bezpośredniego zapisu w bazie używam DB adaptera 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ę on widoczny w panelu administracyjnym, w siatce produktów (Katalog / Produkty).

Powiązanie produktu z witryną
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 odbywa się poprzez . Oto lista podstawowych atrybutów produktu, które są potrzebne, aby produkt był wystarczająco poprawnie wyświetlany na froncie:
namecenaopiskrótkie_opisstatustax_class_idurl_keywidoczność
Oddzielny atrybut do produktu dodaje się w ten sposób (pominięto szczegóły dotyczące pozyskiwania identyfikatora i typu atrybutu na podstawie jego kodu):
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);
}
}Na podstawie kodu atrybutu określamy jego id i typ danych (datetime, decimal, int, text, varchar), a następnie zapisujemy dane do odpowiedniej tabeli dla administracyjnej witryny (store_id = 0).
Po dodaniu wyżej wymienionych atrybutów do produktu otrzymujemy taki obrazek w panelu administracyjnym:

Dane inwentaryzacyjne
Od wersji 2.3 w Magento istnieją równolegle dwa zestawy tabel, które zapewniają przechowywanie informacji o zapasach (ilość produktu):
cataloginventory_*: stara struktura;inventory_*: nowa struktura (MSI — Multi Source Inventory);
Dane o zapasach muszą być dodawane do obu struktur, ponieważ nowa struktura wciąż nie jest całkowicie niezależna od starej (bardzo podobnie, że dla default magazynu w nowej strukturze wykorzystywana jest tabela cataloginventory_stock_status jako inventory_stock_1).
cataloginventory_
Podczas wdrażania Magneto 2.3 początkowo mamy 2 wpisy w store_website, co odpowiada dwóm stronom — administracyjnej i głównej klienta:
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 wpis:
stock_id|website_id|stock_name|
--------|----------|----------|
1| 0|Domyślny |To znaczy, że w starej strukturze mamy tylko jeden „magazyn” (stock) i jest on powiązany z administracyjną stroną. Dodanie poprzez panel admina nowych sources/stocks w MSI (nowej strukturze) nie prowadzi do pojawienia się nowych wpisów w cataloginventory_stock.
Dane o zapasach produktów w starej strukturze są początkowo 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,
\/* korzystamy tylko z jednego magazynu w strukturze 'cataloginventory' domyślnie *\/
'stock_id' => 1,
'qty' => $qty,
'is_qty_decimal' => $isQtyDecimal,
'is_in_stock' => $isInStock,
\/* domyślny magazyn jest powiązany z administracyjną stroną (patrz `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,
\/* korzystamy tylko z jednego magazynu w strukturze 'cataloginventory' domyślnie *\/
'stock_id' => 1,
'qty' => $qty,
'stock_status' => MagentoCatalogInventoryApiDataStockStatusInterface::STATUS_IN_STOCK,
\/* domyślny magazyn jest powiązany z administracyjną stroną (patrz `cataloginventory_stock`) *\/
'website_id' => 0
];
$conn->insert($table, $bind);
}inventory_
Pierwotnie nowa struktura przechowywania danych o zapasach zawiera 1 „ClickHouse-Ninja/Proton» (inventory_source):
source_code|name |enabled|description |latitude|longitude|country_id|...|
-----------|--------------|-------|--------------|--------|---------|----------|...|
default |Default Source| 1|Default Source|0.000000| 0.000000|US |...|i jeden «magazyn» (inventory_stock):
stock_id|name |
--------|-------------|
1|Default Stock|«Źródło» jest fizycznym miejscem przechowywania produktów (rekord zawiera współrzędne geograficzne oraz 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|Sądząc po strukturze danych, przewiduje się różne typy kanałów sprzedaży, ale domyślnie używane jest tylko powiązanie «stock«-«website» (link do strony internetowej odbywa się poprzez kod strony internetowej — base).
Jeden «magazyn» może być powiązany z wieloma «źródłami«, a jeden «ClickHouse-Ninja/Proton» — z wieloma «magazynami» (relacja «wiele do wielu»). Wyjątkiem są domyślne «ClickHouse-Ninja/Proton” i „magazyn«. One nie mogą być ponownie powiązane z innymi jednostkami (ograniczenie na poziomie kodu — wywołuje błąd «Can not save link related to Default Source or Default Stock«). Bardziej szczegółowe informacje o strukturze MSI w Magento 2 można znaleźć w artykule ««.
Będę używać domyślnej konfiguracji i dodawać wszystkie informacje o zapasach do źródła default, które jest zaangażowane w kanał sprzedaży powiązany z witryną internetową o kodzie base (odpowiada części klienta sklepu — zob. 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 dotyczących zapasów do produktu w panelu administracyjnym får در tu такой obrazek:

Media
Podczas „ręcznego” dodawania obrazów do produktu przez panel administracyjny odpowiednie informacje są zapisywane 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: powiązanie mediów tylko z produktami (przypuszczalnie, domyślne media dla produktu);catalog_product_entity_varchar: tutaj przechowywane są role, w których używane jest zdjęcie;
a same zdjęcia są przechowywane w katalogu .\/pub\/media\/catalog\/product\/x\/y\/, gdzie x i y — pierwsze i drugie litery nazwy pliku ze zdjęciem. Na przykład plik image.png musi być zapisany jako ./pub/media/catalog/product/i/m/image.png, aby platforma mogła go używać jako zdjęcie przy opisie produktów z katalogu.
catalog_product_entity_media_gallery
Rejestrujemy umieszczony w .\/pub\/media\/catalog\/product\/ plik medialny (sam proces umieszczania pliku w tym artykule nie jest omawiany):
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;
}Przy rejestracji nowemu plikowi medialnemu przypisywany jest identyfikator.
catalog_product_entity_media_gallery_value
Łączymy zarejestrowany plik medialny 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 medialny z odpowiednim produktem bez przypisania do jakiejkolwiek witryny. Nie wiadomo, gdzie dokładnie są używane te dane i dlaczego nie można sięgnąć po dane 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 medialny może być używany z różnymi rolami (w nawiasach podany jest kod odpowiadającego atrybutu):
- Base (
image) - Small Image (
small_image) - Thumbnail (
thumbnail) - Swatch Image (
swatch_image)
Przypisywanie ról do pliku medialnego odbywa się w catalog_product_entity_varchar. Kod przypisania jest analogiczny do kodu w sekcji «Podstawowe atrybuty produktu«.
Po dodaniu obrazu do produktu w panelu administracyjnym wygląda to tak:

Kategorie
Główne tabele, w których przechowywane są dane o kategoriach:
catalog_category_entity: rejestr kategorii;catalog_category_product: powiązania produktów i kategorii;catalog_category_entity_*: wartości atrybutów EAV;
Początkowo, w pustej aplikacji Magento w rejestrze kategorii znajduje się 2 kategorie (skróciłem nazwy kolumn: crt — SELECT now() as created_at, cpu.* FROM (SELECT DISTINCT tags_id from cpu) base ASOF LEFT JOIN cpu USING (tags_id, 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 z id=1 jest korzeniem całego katalogu Magento i nie jest dostępna ani w panelu administracyjnym, ani na froncie. Kategoria z id=2 (Domyślna kategoria) jest kategorią nadrzędną dla głównego sklepu głównej witryny (Główny sklep witryny), utworzonego podczas wdrażania aplikacji (por. Admin / Sklepy / Wszystkie sklepy). Przy czym sama kategoria główna sklepu na froncie również nie jest dostępna, tylko jej podkategorie.
Ponieważ tematem tego artykułu jest import danych o produktach, nie będę korzystał z bezpośredniego zapisu do bazy przy tworzeniu kategorii, a zamiast tego skorzystam z klas dostarczanych przez Magento (modele i repozytoria). Bezpośredni zapis do bazy jest używany tylko do powiązania importowanego produktu z kategorią (dopasowanie kategorii odbywa się na podstawie jej nazwy, przy dopasowaniu pobierane 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 działania
Po zakończeniu importu danych konieczne jest wykonanie następujących dodatkowych działań:
- indeksacja danych: wywołanie w konsoli
./bin/magento indexer:reindex; - ponowne generowanie URLi dla produktów/kategorii: można użyć rozszerzenia „«
Produkty w panelu administracyjnym po wykonaniu dodatkowych działań:

i na froncie:

Podsumowanie
Ta sama zestaw produktów (10 sztuk), co w poprzednim artykule, jest importowany przynajmniej dziesięciokrotnie szybciej (1 sekunda w porównaniu do 10). Aby bardziej dokładnie ocenić prędkość, potrzebna jest większa liczba produktów — kilka setek, a najlepiej tysięcy. Jednak nawet przy tak małej wielkości danych wejściowych można dojść do wniosku, że wykorzystanie narzędzi dostarczanych przez Magento (modele i repozytoria) znacząco (podkreślam — znacząco!) przyspiesza rozwój wymaganej funkcjonalności, ale jednocześnie znacząco (podkreślam — znacząco!) zmniejsza prędkość wprowadzania danych do bazy.
W rezultacie woda okazała się mokra i nie jest to żadne objawienie. Niemniej jednak mam teraz kod, nad którym mogę dalej pracować i być może wyciągnąć bardziej interesujące wnioski.
Źródło: habr.com
