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

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

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

Kod, który powstał w wyniku pisania 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 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_id
  • sku

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

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

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

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 odbywa się poprzez EAV-atributy. Oto lista podstawowych atrybutów produktu, które są potrzebne, aby produkt był wystarczająco poprawnie wyświetlany na froncie:

  • name
  • cena
  • opis
  • krótkie_opis
  • status
  • tax_class_id
  • url_key
  • widoczność

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:

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

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

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:

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

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.

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.

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

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

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

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: crtSELECT now() as created_at, cpu.* FROM (SELECT DISTINCT tags_id from cpu) base ASOF LEFT JOIN cpu USING (tags_id, created_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 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:

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

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

Produkty w panelu administracyjnym po wykonaniu dodatkowych działań:

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

i na froncie:

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

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

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