Magento 2 : importer des produits directement dans la base de données

В article précédent J'ai décrit le processus d'importation de produits dans Magento 2 de la manière habituelle - via des modèles et des référentiels. La méthode habituelle a une vitesse de traitement des données très faible. Mon ordinateur portable produisait environ un produit par seconde. Dans cette suite, j'envisage une manière alternative d'importer un produit - par entrée directe dans la base de données, en contournant les mécanismes standards de Magento 2 (modèles, usines, référentiels). La séquence d'étapes pour importer des produits peut être adaptée à n'importe quel langage de programmation pouvant fonctionner avec MySQL.

Clause de non-responsabilité  : Magento a des fonctionnalités prêtes à l'emploi pour importation de données et, très probablement, cela vous suffira. Cependant, si vous avez besoin d'un contrôle plus complet sur le processus d'importation, sans vous limiter à la préparation d'un fichier CSV pour ce que vous avez, bienvenue sur cat.

Magento 2 : importer des produits directement dans la base de données

Le code résultant de la rédaction des deux articles est consultable dans le module Magento "flancer32/mage2_ext_demo_import". Voici quelques restrictions que j'ai suivies pour simplifier le code du module de démonstration :

  • Les produits sont uniquement créés, pas mis à jour.
  • Un entrepôt
  • Seuls les noms de catégories sont importés, sans leur structure
  • Les structures de données sont conformes à la version 2.3

JSON pour importer un seul produit :

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

Aperçu des principales étapes de l'importation

  • enregistrement du produit lui-même
  • connexion entre le produit et le site Web
  • attributs de base du produit (EAV)
  • données d'inventaire (quantité de produit en stock)
  • médias (images)
  • connexion avec les catégories du catalogue

Enregistrement du produit

Des informations de base sur le produit peuvent être trouvées dans 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`)
)

Les informations minimales requises pour créer une entrée dans le registre du produit sont :

  • attribute_set_id
  • sku

supplémentaire:

  • type_id — si nous ne le précisons pas, alors 'simple' sera utilisé

Pour écrire directement dans la base de données, j'utilise l'adaptateur DB de Magento lui-même :

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

Après avoir enregistré le produit auprès de catalog_product_entity il devient visible dans le panneau d'administration, dans la grille des produits (Catalogue/Produits).

Magento 2 : importer des produits directement dans la base de données

Relation entre le produit et le site Web

L'association du produit au site détermine dans quels magasins et vitrines le produit sera disponible en vitrine.

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 : importer des produits directement dans la base de données

Attributs de base du produit

Le produit nouvellement enregistré n'a pas encore de nom ni de description. Tout cela se fait à travers Attributs EAV. Voici une liste des attributs de base du produit qui sont nécessaires pour que le produit s'affiche correctement sur le devant :

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

Un attribut distinct est ajouté à un produit comme celui-ci (les détails d'obtention de l'identifiant et du type de l'attribut à partir de son code sont omis) :

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

À l'aide du code d'attribut, nous déterminons son identifiant et son type de données (datetime, decimal, int, text, varchar), puis écrivez les données de la fenêtre d'administration dans la table appropriée (store_id = 0).

Après avoir ajouté les attributs ci-dessus au produit, vous obtenez cette image dans le panneau d'administration :

Magento 2 : importer des produits directement dans la base de données

Données d'inventaire

À partir de la version 2.3 dans Magento, il existe deux ensembles parallèles de tables qui permettent de stocker les informations d'inventaire (quantité de produit) :

  • cataloginventory_*: structure ancienne ;
  • inventory_*: nouvelle structure (MSI - Multi Source Inventory) ;

Vous devez ajouter des données d'inventaire aux deux structures, car la nouvelle structure n'est pas encore complètement indépendante de l'ancienne (il est très probable que pour default entrepôt dans la nouvelle structure, une table est impliquée cataloginventory_stock_status comme inventory_stock_1).

catalogueinventaire_

Lors du déploiement de Magneto 2.3, nous avons initialement 2 entrées dans store_website, ce qui correspond à deux sites - administratif et client principal :

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

Dans la table cataloginventory_stock nous n'avons qu'une seule entrée :

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

Autrement dit, dans notre ancienne structure, il n'y a qu'un seul « entrepôt » (stock) et il est lié au site administratif. En ajouter de nouveaux via le panneau d'administration sources/stocks dans MSI (nouvelle structure) n'entraîne pas de nouvelles entrées dans cataloginventory_stock.

Les données d'inventaire sur les produits de l'ancienne structure sont initialement enregistrées dans des tableaux :

  • cataloginventory_stock_item
  • cataloginventory_stock_status

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

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

inventaire_

Initialement, la nouvelle structure de stockage des données d'inventaire contient 1 "source"(inventory_source):

source_code|name          |enabled|description   |latitude|longitude|country_id|...|
-----------|--------------|-------|--------------|--------|---------|----------|...|
default    |Default Source|      1|Default Source|0.000000| 0.000000|US        |...|

et une "entrepôt"(inventory_stock):

stock_id|name         |
--------|-------------|
       1|Default Stock|

«Source» représente le stockage physique des produits (l'enregistrement contient les coordonnées physiques et l'adresse postale). "Entrepôt" est une union logique de plusieurs " sources " (inventory_source_stock_link)

link_id|stock_id|source_code|priority|
-------|--------|-----------|--------|
      1|       1|default    |       1|

au niveau où s'effectue la connexion au canal de vente (inventory_stock_sales_channel)

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

À en juger par la structure des données, différents types de canaux de vente sont supposés, mais par défaut uniquement la connexion "stock"-"site de NDN Collective"(le lien vers le site suit le code du site - base).

Un "entrepôt"peut être lié à plusieurs"les sources"et une "source" - à plusieurs "entrepôts"(relation plusieurs-à-plusieurs). Les exceptions sont par défaut "source"Et"entrepôt". Ils ne sont pas ré-liés à d'autres entités (limitation au niveau du code - l'erreur "Impossible d'enregistrer le lien lié à la source par défaut ou au stock par défaut"). Plus de détails sur la structure MSI dans Magento 2 peuvent être trouvés dans l'article «Système de gestion d'entrepôt utilisant CQRS et Event Sourcing. Conception«.

J'utiliserai la configuration par défaut et ajouterai toutes les informations d'inventaire à la source default, qui intervient dans le canal de vente associé au site Internet avec le code base (correspond à la devanture du magasin - voir 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);
}

Après avoir ajouté les données d'inventaire au produit dans le panneau d'administration, vous obtenez cette image :

Magento 2 : importer des produits directement dans la base de données

médias

Lors de l'ajout « manuel » d'une image à un produit via le panneau d'administration, les informations pertinentes sont consignées dans les tableaux suivants :

  • catalog_product_entity_media_gallery: registre des médias (images et fichiers vidéo) ;
  • catalog_product_entity_media_gallery_value: relier les médias aux produits et aux vitrines (localisation) ;
  • catalog_product_entity_media_gallery_value_to_entity: relier les médias aux produits uniquement (vraisemblablement du contenu multimédia par défaut pour le produit) ;
  • catalog_product_entity_varchar: Les rôles dans lesquels l'image est utilisée sont stockés ici ;

et les images elles-mêmes sont enregistrées dans le répertoire ./pub/media/catalog/product/x/y/x и y — les première et deuxième lettres du nom du fichier image. Par exemple, fichier image.png doit être enregistré sous ./pub/media/catalog/product/i/m/image.png, afin que la plateforme puisse l'utiliser comme image lors de la description des produits du catalogue.

S'inscrire publié dans ./pub/media/catalog/product/ fichier multimédia (le processus de placement du fichier lui-même n'est pas abordé dans cet article) :

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

Une fois enregistré, un nouveau fichier multimédia se voit attribuer un identifiant.

Nous associons le fichier média enregistré au produit correspondant pour la vitrine par défaut :

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

Nous associons le fichier multimédia enregistré au produit correspondant sans être lié à une quelconque vitrine. On ne sait pas exactement où ces données sont utilisées et pourquoi il est impossible d'accéder aux données du tableau précédent, mais ce tableau existe et les données y sont écrites lorsqu'une image est ajoutée au produit. Alors c'est tout.

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

Un fichier média peut être utilisé avec différents rôles (le code d'attribut correspondant est indiqué entre parenthèses) :

  • Base(image)
  • Petite image (small_image)
  • La vignette (thumbnail)
  • Image d'échantillon (swatch_image)

Lier des rôles à un fichier multimédia est exactement ce qui se passe dans catalog_product_entity_varchar. Le code de liaison est similaire au code du "Attributs de base du produit«.

Après avoir ajouté une image au produit dans le panneau d'administration, elle ressemble à ceci :

Magento 2 : importer des produits directement dans la base de données

catégorie

Principaux tableaux contenant des données par catégorie :

  • catalog_category_entity: registre des catégories;
  • catalog_category_product: connexion entre produits et catégories ;
  • catalog_category_entity_*: valeurs d'attribut EAV ;

Initialement, dans une application Magento vide, le registre des catégories contient 2 catégories (j'ai raccourci les noms des colonnes : 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|

La catégorie avec id=1 est la racine de l'ensemble du catalogue Magento et n'est disponible ni dans le panneau d'administration ni sur la page d'accueil. Catégorie avec id=2 (Catégorie par défaut) est la catégorie racine du magasin principal du site principal (Boutique du site Web principal) créé lors du déploiement de l'application (voir. Admin / Magasins / Tous les magasins). De plus, la catégorie racine du magasin lui-même n’est pas non plus disponible à l’avant, uniquement ses sous-catégories.

Le sujet de cet article étant toujours l'importation de données sur les produits, je n'utiliserai pas la saisie directe dans la base de données lors de la création de catégories, mais j'utiliserai les classes fournies par Magento lui-même (modèles et référentiels). La saisie directe dans la base de données permet uniquement d'associer le produit importé à une catégorie (la catégorie est mise en correspondance par son nom, et l'identifiant de la catégorie est récupéré lors de la mise en correspondance) :

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

Après avoir ajouté un lien de produit vers les catégories « Catégorie 1 » et « Catégorie 2 », les détails du produit dans le panneau d'administration ressemblent à ceci :

Magento 2 : importer des produits directement dans la base de données

Actions supplémentaires

Une fois l'importation des données terminée, vous devez effectuer les étapes supplémentaires suivantes :

  • indexation des données : appel dans la console ./bin/magento indexer:reindex;
  • régénération des URL de produits/catégories : vous pouvez utiliser l'extension "elgentos/regenerate-catalog-urls«

Produits dans le panneau d'administration après avoir effectué des actions supplémentaires :

Magento 2 : importer des produits directement dans la base de données

et à l'avant :

Magento 2 : importer des produits directement dans la base de données

Résumé

Le même ensemble de produits (10 pièces) que dans l'article précédent est importé au moins un ordre de grandeur plus rapidement (1 seconde contre 10). Pour estimer plus précisément la vitesse, vous avez besoin d'un plus grand nombre de produits - plusieurs centaines, voire des milliers. Cependant, même avec une si petite taille de données d'entrée, on peut conclure que l'utilisation des outils fournis par Magento (modèles et référentiels) est significative (j'insiste - significativement!) accélérer le développement des fonctionnalités requises, mais en même temps de manière significative (j'insiste - significativement!) Réduisez la vitesse à laquelle les données entrent dans la base de données.

Du coup, l'eau s'est avérée humide et ce n'est pas une révélation. Cependant, j'ai maintenant le code avec lequel jouer et peut-être arriver à des conclusions plus intéressantes.

Source: habr.com