Magento 2: importación de productos directamente a la base de datos

En artículo anterior He descrito el proceso de importación de productos en Magento 2 de la manera estándar: a través de modelos y repositorios. Este método habitual tiene una velocidad de procesamiento de datos bastante baja. En mi portátil, se lograba aproximadamente un producto por segundo. En esta continuación, examino un método alternativo de importación de productos: registrándolos directamente en la base de datos, eludiendo los mecanismos estándar de Magento 2 (modelos, fábricas, repositorios). La secuencia de pasos que facilita la importación de productos puede adaptarse a cualquier lenguaje de programación que pueda trabajar con MySQL.

Descargo de responsabilidad: Magento cuenta con funcionalidad lista para importar datos y, probablemente, le será suficiente. Sin embargo, si necesita un control más completo sobre el proceso de importación, que no se limite a preparar un archivo CSV para lo que ya existe, bienvenido a la continuación.

Magento 2: importación de productos directamente a la base de datos

El código resultante de escribir ambos artículos se puede ver en el módulo de Magento «flancer32/mage2_ext_demo_import». Aquí hay algunas limitaciones que seguí para simplificar el código del módulo de demostración:

  • Los productos solo se crean, no se actualizan.
  • Un almacén
  • Solo se importan los nombres de las categorías, sin su estructura.
  • Las estructuras de datos corresponden a la versión 2.3.

JSON para importar un producto individual:

{
  "sku": "MVA20D-UBV-3",
  "name": "Tapón para sellado VA47-29 IEK",
  "desc": "Proporciona acceso a dispositivos ...",
  "desc_short": "El tapón para sellado VA47-29 IEK está diseñado para ...",
  "price": 5.00,
  "qty": 25,
  "categories": ["Categoría 1", "Categoría 2"],
  "image_path": "mva20d_ubv_3.png"
}

Panorama de las etapas principales de la importación

  • registro del propio producto
  • vinculación del producto y el sitio web
  • atributos básicos del producto (EAV)
  • datos de inventario (cantidad de producto en el almacén)
  • medios (imágenes)
  • vinculación con las categorías del catálogo

Registro del producto

La información básica sobre el producto se encuentra en catalog_product_entity:

CREATE TABLE `catalog_product_entity` (
  `entity_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'Id de entidad',
  `attribute_set_id` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT 'ID del conjunto de atributos',
  `type_id` varchar(32) NOT NULL DEFAULT 'simple' COMMENT 'ID del tipo',
  `sku` varchar(64) DEFAULT NULL COMMENT 'SKU',
  `has_options` smallint(6) NOT NULL DEFAULT '0' COMMENT 'Tiene opciones',
  `required_options` smallint(5) unsigned NOT NULL DEFAULT '0' COMMENT 'Opciones requeridas',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Hora de creación',
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Hora de actualización',
  PRIMARY KEY (`entity_id`),
  KEY `CATALOG_PRODUCT_ENTITY_ATTRIBUTE_SET_ID` (`attribute_set_id`),
  KEY `CATALOG_PRODUCT_ENTITY_SKU` (`sku`)
)

Información mínima necesaria para crear un registro en el registro de productos:

  • attribute_set_id
  • sku

adicional:

  • type_id — si no se especifica, se usará 'simple'

Para escribir directamente en la base de datos, utilizo el adaptador DB de 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;
}

Después de registrar el producto en catalog_product_entity se vuelve visible en el backend, en la cuadrícula de productos (Catálogo / Productos).

Magento 2: importación de productos directamente a la base de datos

Vinculación del producto con el sitio web

La vinculación del producto con el sitio determina en qué tiendas y en qué vitrinas el producto estará disponible en el frontend.

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: importación de productos directamente a la base de datos

Atributos básicos del producto

Un producto recién registrado aún no tiene nombre ni descripción. Todo esto se hace a través de atributos EAV. Aquí hay una lista de los atributos básicos del producto que son necesarios para que el producto se muestre de manera suficiente en el frontend:

  • name
  • precio
  • descripción
  • corta_descripcion
  • status
  • tax_class_id
  • url_key
  • visibilidad

Un atributo adicional al producto se agrega de esta manera (se omiten los detalles para obtener el identificador y el tipo del atributo por su código):

public function create($prodId, $attrCode, $attrValue)
{
    $attrId = \/\* obtener ID del atributo por código de atributo *\/ 
    $attrType = \/\* obtener tipo de atributo [datetime|decimal|int|text|varchar] por código de atributo *\/ 
    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,
            \/\* poner todos los atributos en la vista de la tienda predeterminada con id=0 (administrador) *\/ 
            'store_id' => 0,
            'value' => $attrValue
        ];
        $conn->insert($table, $bind);
    }
}

Por el código del atributo determinamos su id y tipo de datos (datetime, decimal, int, text, varchar), luego escribimos los datos en la tabla correspondiente para la vista administrativa (store_id = 0).

Después de agregar los atributos mencionados anteriormente al producto, se obtiene la siguiente imagen en el backend:

Magento 2: importación de productos directamente a la base de datos

Datos de inventario

A partir de la versión 2.3 en Magento, existen dos conjuntos de tablas que gestionan la información de inventario (cantidad de producto):

  • cataloginventory_*: estructura antigua;
  • inventory_*: nueva estructura (MSI — Inventario de Múltiples Fuentes);

Es necesario añadir datos de inventario a ambas estructuras, ya que la nueva estructura aún no es completamente independiente de la antigua (es muy probable que para default el almacén en la nueva estructura se utilice la tabla cataloginventory_stock_status como inventory_stock_1).

cataloginventory_

Al desplegar Magneto 2.3, inicialmente tenemos 2 registros en store_website, que corresponden a dos sitios: el administrativo y el principal del cliente:

website_id|code |name        |sort_order|default_group_id|is_default|
----------|-----|------------|----------|----------------|----------|
         0|admin|Admin       |         0|               0|         0|
         1|base |Sitio Principal|         0|               1|         1|

En la tabla cataloginventory_stock tenemos solo un registro:

stock_id|website_id|stock_name|
--------|----------|----------|
       1|         0|Predeterminado   |

Es decir, en nuestra estructura antigua solo hay un "almacén" (stock) y está vinculado al sitio web administrativo. La adición a través del panel de administración de nuevos fuentes/stocks en MSI (nueva estructura) no genera nuevos registros en cataloginventory_stock.

Los datos de inventario de productos en la estructura antigua se registran inicialmente en las tablas:

  • 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,
        /* usamos solo un almacén en la estructura 'cataloginventory' por defecto */
        'stock_id' => 1,
        'qty' => $qty,
        'is_qty_decimal' => $isQtyDecimal,
        'is_in_stock' => $isInStock,
        /* el almacén predeterminado está vinculado al sitio web del admin (ver `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,
        /* usamos solo un almacén en la estructura 'cataloginventory' por defecto */
        'stock_id' => 1, 
        'qty' => $qty,
        'stock_status' => MagentoCatalogInventoryApiDataStockStatusInterface::STATUS_IN_STOCK,
        /* el almacén predeterminado está vinculado al sitio web del admin (ver `cataloginventory_stock`) */
        'website_id' => 0 
    ];
    $conn->insert($table, $bind);
}

inventory_

Inicialmente, la nueva estructura para el almacenamiento de datos de inventario contiene 1 "fuente» (inventory_source):

código_fuente|nombre          |habilitado|descripción   |latitud|longitud|id_pais|...|
-----------|--------------|-------|--------------|--------|---------|----------|...|
defecto    |Fuente por Defecto|      1|Fuente por Defecto|0.000000| 0.000000|US        |...|

y uno «almacén» (inventario_existente):

id_stock|nombre         |
--------|-------------|
       1|Stock por Defecto|

«Fuente» representa un almacenamiento físico para productos (el registro contiene coordenadas físicas y dirección postal). «Almacén» representa una unión lógica de varias «fuentes» (enlace_inventario_fuente_stock)

id_enlace|id_stock|código_fuente|prioridad|
-------|--------|-----------|--------|
      1|       1|defecto    |       1|

a nivel del cual se realiza la vinculación al canal de venta (inventario_stock_canal_ventas)

tipo   |código|id_stock|
-------|----|--------|
website|base|       1|

Según la estructura de datos se suponen diferentes tipos de canales de venta, pero por defecto solo se utiliza la relación «stock«-«sitio_web» (el enlace al sitio web se realiza mediante el código del sitio web — base).

Un «almacén» puede estar vinculado a varias «fuentes«, y un «fuente» — a varios «almacenes» (relación «muchos a muchos»). Las excepciones son los «default» «fuente» y «almacén«. No se vuelven a vincular a otras entidades (restricción a nivel de código — genera un error «No se puede guardar el enlace relacionado con la Fuente por Defecto o el Stock por Defecto«). Para más información sobre la estructura MSI en Magento 2, se puede leer el artículo «Sistema de gestión de almacenes utilizando CQRS y Event Sourcing. Diseño«.

Utilizaré la configuración por defecto y agregaré toda la información de inventario a la fuente default, que está involucrada en el canal de venta relacionado con el sitio web con código base (corresponde a la parte del cliente de la tienda — ver. 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);
}

Después de agregar los datos de inventario al producto en la administración, se obtiene la siguiente imagen:

Magento 2: importación de productos directamente a la base de datos

Medios

Al añadir manualmente imágenes al producto a través de la administración, la información correspondiente se registra en las siguientes tablas:

  • catalog_product_entity_media_gallery: registro de medios (imágenes y archivos de video);
  • catalog_product_entity_media_gallery_value: vinculación de medios a productos y vitrinas (localización);
  • catalog_product_entity_media_gallery_value_to_entity: vinculación de medios solo a productos (presumiblemente, contenido multimedia por defecto para el producto);
  • catalog_product_entity_varchar: aquí se almacenan los roles en los que se utiliza la imagen;

y las imágenes en sí se almacenan en el catálogo ./pub/media/catalog/product/x/y/, donde x y y — las dos primeras letras del nombre del archivo de imagen. Por ejemplo, el archivo image.png debe guardarse como ./pub/media/catalog/product/i/m/image.png, para que la plataforma pueda utilizarlo como imagen al describir productos del catálogo.

Registramos lo alojado en ./pub/media/catalog/product/ archivo multimedia (el proceso de alojamiento del archivo en este artículo no se considera):

function createMediaGallery($imgPathPrefixed)
{
    $attrId = /* obtener ID de atributo por código de atributo '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' o 'video' */
        'media_type' => 'image',
        'disabled' => false
    ];
    $conn->insert($table, $bind);
    $result = $conn->lastInsertId($table);
    return $result;
}

Al registrar un nuevo archivo multimedia se le asigna un identificador.

Vinculamos el archivo multimedia registrado con el producto correspondiente para la vitrina predeterminada:

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,
        /* usar vista de tienda admin por defecto */
        'store_id' => 0,
        'entity_id' => $prodId,
        'label' => null,
        /* solo tenemos una imagen */
        'position' => 1,
        'disabled' => false
    ];
    $conn->insert($table, $bind);
}

Vinculamos el archivo multimedia registrado con el producto sin vinculaciones a ninguna vitrina. No está claro dónde se utilizan realmente estos datos y por qué no se pueden utilizar los datos de la tabla anterior, pero esta tabla existe y los datos se escriben en ella al agregar una imagen al producto. Por lo tanto, así es.

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

El archivo multimedia puede utilizarse con diferentes roles (el código correspondiente del atributo se indica entre paréntesis):

  • Base (image)
  • Imagen pequeña (small_image)
  • Miniatura (thumbnail)
  • Imagen de muestra (swatch_image)

La vinculación de roles al archivo multimedia ocurre precisamente en catalog_product_entity_varchar. El código de vinculación es similar al del apartado «Atributos básicos del producto«.

Después de agregar una imagen al producto en el panel de administración se ve así:

Magento 2: importación de productos directamente a la base de datos

Categorías

Tablas principales que contienen datos por categorías:

  • catalog_category_entity: registro de categorías;
  • catalog_category_product: relación entre productos y categorías;
  • catalog_category_entity_*: valores de atributos EAV;

Inicialmente, en una aplicación Magento vacía, el registro de categorías contiene 2 categorías (he abreviado los nombres de las columnas: 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 categoría con id=1 es la raíz de todo el catálogo de Magento y no está disponible ni en el panel de administración ni en la parte frontal. La categoría con id=2 (Default Category) es la categoría raíz para la tienda principal del sitio principal (Main Website Store), que se crea al desplegar la aplicación (ver Admin / Stores / All Stores). Además, la categoría raíz de la tienda en la parte frontal tampoco está disponible, solo sus subcategorías.

Dado que el tema de este artículo es la importación de datos de productos, no utilizaré la escritura directa en la base de datos al crear categorías, sino que emplearé las clases proporcionadas por Magento (modelos y repositorios). La escritura directa en la base se utiliza solo para relacionar el producto importado con la categoría (la asignación de categoría se realiza por su nombre, al asignar se extrae el id de la categoría):

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

Después de agregar la relación del producto con las categorías «Categoría 1» y «Categoría 2», los detalles del producto en el panel de administración se ven aproximadamente así:

Magento 2: importación de productos directamente a la base de datos

Acciones adicionales

Después de completar la importación de datos, se deben realizar las siguientes acciones adicionales:

  • indexación de datos: llamada en la consola ./bin/magento indexer:reindex;
  • regeneración de URL para productos/categorías: se puede usar la extensión «elgentos/regenerate-catalog-urls«

Productos en el panel de administración después de realizar acciones adicionales:

Magento 2: importación de productos directamente a la base de datos

y en la parte frontal:

Magento 2: importación de productos directamente a la base de datos

Currículum

El mismo conjunto de productos (10 piezas) que en el artículo anterior se importa, al menos, de manera mucho más rápida (1 segundo frente a 10). Para una evaluación más precisa de la velocidad, se necesita una mayor cantidad de productos: varios cientos, o mejor aún, miles. Sin embargo, incluso con un tamaño tan pequeño de los datos de entrada, se puede concluir que el uso de las herramientas proporcionadas por Magento (modelos y repositorios) acelera significativamente (enfatizo — significativamente!) el desarrollo de la funcionalidad requerida, pero al mismo tiempo reduce significativamente (enfatizo — significativamente!) la velocidad a la que los datos ingresan a la base de datos.

Como resultado, el agua resultó estar mojada y no es ninguna revelación. Sin embargo, ahora tengo el código para seguir experimentando y, posiblemente, hacer conclusiones más interesantes.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster