Magento 2: import produktów bezpośrednio do bazy danych

W poprzednim artykule 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 importu danych 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.

Magento 2: import produktów bezpośrednio do bazy danych

Kod utworzony w wyniku napisania obu artykułów można zobaczyć w module Magento „flancer32/mage2_ext_demo_import“. 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_id
  • sku

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

Magento 2: import produktów bezpośrednio do bazy danych

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

Magento 2: import produktów bezpośrednio do bazy danych

Podstawowe atrybuty produktu

Nowo zarejestrowany produkt nie ma jeszcze ani nazwy, ani opisu. Wszystko to realizowane jest przez atrybuty EAV. Oto lista podstawowych atrybutów produktu, które są potrzebne, aby produkt był odpowiednio wyświetlany na froncie:

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

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:

Magento 2: import produktów bezpośrednio do bazy danych

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_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,
        /* 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 „System zarządzania magazynem z wykorzystaniem CQRS i Event Sourcing. Projektowanie«.

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:

Magento 2: import produktów bezpośrednio do bazy danych

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.

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.

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

Łą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:

Magento 2: import produktów bezpośrednio do bazy danych

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: crtcreated_at, updupdated_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:

Magento 2: import produktów bezpośrednio do bazy danych

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 „elgentos/regenerate-catalog-urls«

Produkty w panelu administracyjnym po wykonaniu dodatkowych czynności:

Magento 2: import produktów bezpośrednio do bazy danych

i na froncie:

Magento 2: import produktów bezpośrednio do bazy danych

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

Kup solidny hosting dla stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting dla stron z ochroną przed DDoS, serwery VPS VDS | ProHoster