Magento 2: importuj produkty bezpośrednio do bazy danych

В Poprzedni artykuł Opisałem proces importu produktów do Magento 2 w standardowy sposób – poprzez modele i repozytoria. Zwykła metoda ma bardzo niską prędkość przetwarzania danych. Mój laptop wytwarzał około jednego produktu na sekundę. W tej kontynuacji rozważam alternatywny sposób importu produktu - poprzez bezpośredni wpis do bazy danych, z pominięciem standardowych mechanizmów Magento 2 (modele, fabryki, repozytoria). Kolejność kroków importu produktów można dostosować do dowolnego języka programowania współpracującego z MySQL.

Odpowiedzialność: Magento posiada gotową funkcjonalność dla import danych i najprawdopodobniej to ci wystarczy. Jeśli jednak potrzebujesz pełniejszej kontroli nad procesem importu, nie ograniczającej się do przygotowania pliku CSV dla tego, co posiadasz, zapraszamy do cat.

Magento 2: importuj produkty bezpośrednio do bazy danych

Kod powstały w wyniku napisania obu artykułów można obejrzeć w module Magento”flacer32/mage2_ext_demo_import„. Oto kilka ograniczeń, których przestrzegałem, aby uprościć kod modułu demonstracyjnego:

  • Produkty są jedynie tworzone, a nie aktualizowane.
  • Jeden magazyn
  • Importowane są tylko nazwy kategorii, bez ich struktury
  • Struktury danych są zgodne z wersją 2.3

JSON do importowania pojedynczego produktu:

{
  "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"
}

Przegląd głównych etapów importu

  • rejestracja samego produktu
  • połączenie pomiędzy produktem a stroną internetową
  • podstawowe cechy produktu (EAV)
  • dane magazynowe (ilość produktu w magazynie)
  • multimedia (zdjęcia)
  • powiązanie z kategoriami katalogowymi

Rejestracja produktu

Podstawowe informacje o produkcie można znaleźć w 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`)
)

Minimalne informacje wymagane do utworzenia wpisu do rejestru wyrobów to:

  • attribute_set_id
  • sku

dodatkowy:

  • type_id — jeśli tego nie określimy, zostanie użyte słowo „prosty”.

Aby pisać bezpośrednio do bazy danych, używam adaptera DB samego 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).

Magento 2: importuj produkty bezpośrednio do bazy danych

Relacja pomiędzy produktem a stroną internetową

Powiązanie produktu z witryną decyduje o tym, w jakich sklepach i na ekspozycjach 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);
}

Magento 2: importuj produkty bezpośrednio do bazy danych

Podstawowe cechy produktu

Nowo zarejestrowany produkt nie ma jeszcze nazwy ani opisu. Wszystko to odbywa się poprzez Atrybuty EAV. Oto lista podstawowych atrybutów produktu, które są potrzebne, aby produkt poprawnie prezentował się na froncie:

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

Do takiego produktu dodawany jest osobny atrybut (pomijane są szczegóły dotyczące uzyskania identyfikatora i typu atrybutu z 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);
    }
}

Za pomocą kodu atrybutu określamy jego identyfikator i typ danych (datetime, decimal, int, text, varchar), następnie wpisz dane dla okna administracyjnego do odpowiedniej tabeli (store_id = 0).

Po dodaniu powyższych atrybutów do produktu w panelu administracyjnym pojawia się taki obrazek:

Magento 2: importuj produkty bezpośrednio do bazy danych

Dane inwentarza

Począwszy od wersji 2.3 w Magento istnieją dwa równoległe zestawy tabel, które umożliwiają przechowywanie informacji o stanie magazynowym (ilości produktu):

  • cataloginventory_*: stara konstrukcja;
  • inventory_*: nowa struktura (MSI – Multi Source Inventory);

Trzeba dodać dane inwentaryzacyjne do obu struktur, bo nowa struktura nie jest jeszcze całkowicie niezależna od starej (jest bardzo prawdopodobne, że dla default magazyn w nowej strukturze zaangażowany jest stół cataloginventory_stock_status jak inventory_stock_1).

kataloginwentarz_

Podczas wdrażania Magneto 2.3 początkowo mamy 2 wpisy store_website, co odpowiada dwóm stronom - administracyjnemu i głównemu klientowi:

website_id|code |name        |sort_order|default_group_id|is_default|
----------|-----|------------|----------|----------------|----------|
         0|admin|Admin       |         0|               0|         0|
         1|base |Main Website|         0|               1|         1|

Stół cataloginventory_stock mamy tylko jeden wpis:

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

Oznacza to, że w naszej starej strukturze jest tylko jeden „magazyn” (stock) i jest powiązany ze stroną administracyjną. Dodawanie nowych poprzez panel administracyjny sources/stocks w MSI (nowa struktura) nie powoduje nowych wpisów w cataloginventory_stock.

Dane inwentaryzacyjne produktów w starej strukturze ujęte są początkowo w tabelach:

  • cataloginventory_stock_item
  • cataloginventory_stock_status

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

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

spis_

Początkowo nowa struktura przechowywania danych inwentaryzacyjnych zawiera 1 "źródło"(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» reprezentuje fizyczne miejsce przechowywania produktów (rekord zawiera współrzędne fizyczne 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 połączenie z kanałem sprzedaży (inventory_stock_sales_channel)

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

Sądząc po strukturze danych, zakłada się różne rodzaje kanałów sprzedaży, ale domyślnie tylko połączenie „stany magazynowe„-” "(link do strony internetowej następuje po kodzie strony - base).

Jeden "magazyn„można powiązać z kilkoma”źródła"i jeden "źródło" - do kilku "magazyny„(relacja wiele do wielu). Wyjątki są domyślne”źródło"A"magazyn„. Nie są one ponownie powiązane z innymi podmiotami (ograniczenie na poziomie kodu – błąd „Nie można zapisać łącza powiązanego z domyślnym źródłem lub domyślnym zapasem„). Więcej szczegółów na temat struktury MSI w Magento 2 znajdziesz w artykule „System zarządzania magazynem wykorzystujący CQRS i Event Sourcing. Projekt".

Użyję konfiguracji domyślnej i dodam wszystkie informacje o zasobach do źródła default, który bierze udział w kanale sprzedaży powiązanym z witryną z kodem base (odpowiada frontowi 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 magazynowych do produktu w panelu administracyjnym otrzymamy taki obraz:

Magento 2: importuj produkty bezpośrednio do bazy danych

Media

W przypadku „ręcznego” dodawania zdjęcia do produktu poprzez panel administracyjny, odpowiednie informacje zapisywane są w poniższych tabelach:

  • catalog_product_entity_media_gallery: rejestr multimediów (zdjęcia i pliki wideo);
  • catalog_product_entity_media_gallery_value: łączenie mediów z produktami i prezentacjami (lokalizacja);
  • catalog_product_entity_media_gallery_value_to_entity: łączenie multimediów tylko z produktami (prawdopodobnie domyślna zawartość multimedialna dla produktu);
  • catalog_product_entity_varchar: Tutaj przechowywane są role, w jakich obraz jest używany;

a same obrazy są zapisywane w katalogu ./pub/media/catalog/product/x/y/Gdzie x и y — pierwsza i druga litera nazwy pliku obrazu. Na przykład plik image.png należy zapisać jako ./pub/media/catalog/product/i/m/image.png, aby platforma mogła wykorzystać go jako obraz przy opisywaniu produktów z katalogu.

Rejestracja opublikowana w ./pub/media/catalog/product/ plik multimedialny (sam proces umieszczania pliku nie jest omawiany w tym artykule):

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

Po zarejestrowaniu nowemu plikowi multimedialnemu przypisywany jest identyfikator.

Łączymy zarejestrowany plik multimedialny z odpowiednim produktem dla domyślnej witryny sklepowej:

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

Łączymy zarejestrowany plik multimedialny z odpowiednim produktem, nie będąc przywiązanym do żadnej witryny sklepowej. Nie jest jasne, gdzie dokładnie te dane są wykorzystywane i dlaczego nie można uzyskać dostępu do danych z poprzedniej tabeli, ale tabela ta istnieje i dane są do niej zapisywane po dodaniu zdjęcia do produktu. Więc to jest to.

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

katalog_produkt_entity_varchar

Pliku multimedialnego można używać w różnych rolach (w nawiasach podano odpowiedni kod atrybutu):

  • Baza (image)
  • Mały obraz (small_image)
  • Miniaturka (thumbnail)
  • Próbka obrazu (swatch_image)

Łączenie ról z plikiem multimedialnym jest dokładnie tym, co dzieje się w catalog_product_entity_varchar. Kod wiążący jest podobny do kodu w „Podstawowe cechy produktu".

Po dodaniu zdjęcia do produktu w panelu administracyjnym wygląda to tak:

Magento 2: importuj produkty bezpośrednio do bazy danych

Kategorie

Główne tabele zawierające dane według kategorii:

  • catalog_category_entity: rejestr kategorii;
  • catalog_category_product: powiązanie pomiędzy produktami i kategoriami;
  • catalog_category_entity_*: wartości atrybutów EAV;

Początkowo w pustej aplikacji Magento rejestr kategorii zawiera 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 podstawą całego katalogu Magento i nie jest dostępna ani w panelu administracyjnym, ani na stronie głównej. Kategoria o identyfikatorze=2 (Domyślny Kategoria) to kategoria główna głównego sklepu witryny głównej (Główny sklep internetowy) utworzone w momencie wdrożenia aplikacji (patrz. Administrator / Sklepy / Wszystkie sklepy). Co więcej, z przodu również nie jest dostępna kategoria główna samego sklepu, a jedynie jego podkategorie.

Ponieważ tematem tego artykułu jest nadal import danych o produktach, przy tworzeniu kategorii nie będę korzystał z bezpośredniego wpisu do bazy, lecz skorzystam z klas dostarczonych przez samo Magento (modele i repozytoria). Bezpośredni wpis do bazy służy jedynie do powiązania zaimportowanego produktu z kategorią (kategoria dopasowywana jest po nazwie, a podczas dopasowywania pobierany jest identyfikator 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 linku do produktu w kategoriach „Kategoria 1” i „Kategoria 2” szczegóły produktu w panelu administracyjnym wyglądają mniej więcej tak:

Magento 2: importuj produkty bezpośrednio do bazy danych

Dodatkowe działania

Po zakończeniu importu danych należy wykonać następujące dodatkowe kroki:

  • indeksowanie danych: wywołaj w konsoli ./bin/magento indexer:reindex;
  • regenerowanie adresów URL dla produktów/kategorii: możesz użyć rozszerzenia „elgentos/regenerate-catalog-urls«

Produkty w panelu administracyjnym po wykonaniu dodatkowych czynności:

Magento 2: importuj produkty bezpośrednio do bazy danych

i z przodu:

Magento 2: importuj produkty bezpośrednio do bazy danych

Streszczenie

Ten sam zestaw produktów (10 sztuk), co w poprzednim artykule, jest importowany co najmniej o rząd wielkości szybciej (1 sekunda w porównaniu z 10). Aby dokładniej oszacować prędkość, potrzebna jest większa liczba produktów – kilkaset, a jeszcze lepiej tysiące. Jednak nawet przy tak małym rozmiarze danych wejściowych możemy stwierdzić, że wykorzystanie narzędzi udostępnianych przez Magento (modele i repozytoria) jest znaczące (podkreślam – dużo!) przyspieszyć rozwój wymaganej funkcjonalności, ale jednocześnie znacznie (podkreślam - dużo!) zmniejsz prędkość, z jaką dane trafiają do bazy danych.

W efekcie woda okazała się mokra i to nie jest żadna rewelacja. Jednak teraz mam kod do pobawienia się i być może dojdę do ciekawszych wniosków.

Źródło: www.habr.com