Cuando encargan una web en drupal a Omitsis es muy habitual que sea una web ya existente y que por lo tanto tenga un contenido que quieran migrar.
En Drupal existe el módulo migrate que está incorporado en el core y que nos permite migrar contenido. Es un módulo muy potente, versátil y que facilita realizar rollbacks de serie. Los rollbacks nos permiten «desimportar», muy útil para cuando aún estamos desarrollando y hay errores.
El módulo migrate permite importar desde diferentes fuentes: un Drupal 7, una base de datos tenga la estructura que sea, desde un csv, un JSON, XML, SOAP, etc.
Es muy común que nos pasen un excel con todos los campos a migrar por cada tipo de contenido. En este post vamos a explicar como realizar este tipo de migración: desde un csv, ya que todas las hojas de cálculo (excel por ejemplo) permite guardar como csv y este es un formato estándar mejor soportado.
Todo lo que vamos a explicar es válido para Drupal moderno. Cuando digo «Drupal moderno» me refiero a Drupal 8 y posteriores (9, 10, 11…), porque a partir del 8 cambió bastante la cosa con la adopción de Symfony y los upgrades entre versiones empezaron a ser más llevaderos. Hoy en día, si estás empezando un proyecto, deberías estar en Drupal 10 o Drupal 11. Drupal 7, 8 y 9 ya no tienen soporte, así que si aún tienes algo ahí… bueno, ya tienes otra cosa en la lista de tareas.
Lo primero es instalar los módulos que serán necesarios. El migrate ya está en el core de Drupal pero vamos a necesitar algunos más: migrate_plus, migrate_tools y migrate_source_csv.
Instalamos estos módulos usando composer:
composer require drupal/migrate_plus drupal/migrate_tools drupal/migrate_source_csvY los activamos, con drush sería así:
drush -y en migrate migrate_plus migrate_tools migrate_source_csvAhora hemos de crear los ficheros yml que definirán las migraciones. Esto lo hemos de hacer dentro de un módulo custom, que podemos tener ya o podemos crear uno específico para el migrate (opción más limpia).
Puedes generar un módulo de forma fácil con la drush, en nuestro caso lo vamos a llamar custom_migrate.
drush generate moduleEn el directorio de este módulo creamos una subcarpeta llamada config y dentro de esta otra llamada install. Ahí pondremos los ficheros yml’s que definirán las migraciones.
La nomenclatura de estos ficheros hay de ser:
migrate_plus.migration.[ID_MIGRACION].ymlPor ejemplo, si queremos importar productos el nombre del fichero podría ser este:
migrate_plus.migration.products.ymlEn este ejemplo el archivo estaría en la siguiente ruta:
web/modules/custom/custom_migrate/config/install/migrate_plus.migration.products.ymlEl módulo migrate_plus nos permite también crear grupos, para luego hacer las importaciones de golpe (y los rollbacks). Puedes ver en este post cómo hacerlo. En este ejemplo no los usaremos, pero es bastante práctico.
Ahora necesitamos un módulo más: config_devel. Este nos permite que Drupal vuelva a leer los ficheros de configuración cuando se lo pidamos, sin tener que estar instalando y desinstalando el módulo cada vez que cambiamos algo. Eso sí, config_devel es solo para entornos de desarrollo, no lo dejes activado en producción.
Para instalarlo:
composer require --dev drupal/config_develY para activarlo:
drush -y en config_develAhora hay que decirle a Drupal que tiene de tener en cuenta el fichero de importación que definimos. Eso se pone en el .info.yml de nuestro módulo. El archivo entero quedaría así:
name: 'Custom migrate'
type: module
description: 'Módulo de importaciones de contenido'
core_version_requirement: ^10 || ^11
package: 'Custom'
dependencies:
- migrate
- migrate_plus
- migrate_tools
- config_devel
config_devel:
install:
- migrate_plus.migration.products
# - migrate_plus.migration.otra_importacion
Ahora nos falta lo más importante: el contenido del yml de importación. Esto daría para muchos posts, así que vamos a poner un ejemplo sencillo:
Primero es necesario definir el id y un label irá bien para ponerle un nombre bonito.
id: products
label: Import productsLuego va la sección del source. Aquí hay que decirle que plugin usaremos y para el caso del csv un path donde estará el csv. En este caso lo hemos puesto dentro del módulo para poderlo integrar en nuestro git.
También le hemos indicado los ids únicos, que en nuestro csv se llamaba «code».
Y finalmente indicamos los campos del csv.
source:
plugin: 'csv'
# Full path to the file.
path: 'modules/custom/custom_migration/data/products.csv'
# Column delimiter. Comma (,) by default.
delimiter: ','
# Field enclosure. Double quotation marks (") by default.
enclosure: '"'
# The row to be used as the CSV header (indexed from 0),
# or null if there is no header row.
header_offset: 0
# The column(s) to use as a key. Each column specified will
# create an index in the migration table and too many columns
# may throw an index size error.
ids:
- code
# Here we identify the columns of interest in the source file.
# Each numeric key is the 0-based index of the column.
# For each column, the key below is the field name assigned to
# the data on import, to be used in field mappings below.
# The label value is a user-friendly string for display by the
# migration UI.
fields:
0:
name: cat
label: 'Categoria'
1:
name: code
label: 'Product code'
2:
name: title
label: 'Title'
3:
name: image
label: 'Image'Ahora la parte del process, dónde indicamos los campos a dónde hay que poner los datos, el plugin que vamos a usar y la fuente (source).
Si es algo muy simple podría ser tan sencillo como esto:
field_nombre_campo_en_nuestro_drupal: nombre_columna_source
Pero muchas veces no es tan simple. Por ejemplo nos pasan el título todo en mayúsculas, así que combinamos varios plugins. También verás la parte de importación a un campo media. Esto lo explicaremos en otro post en breve.
Un caso real podría ser este:
process:
type:
plugin: default_value
default_value: producto
field_cat:
plugin: entity_lookup
source: cat
value_key: name
bundle_key: vid
bundle: cat_producto
entity_type: taxonomy_term
ignore_case: true
field_producto_codigo: code
title:
-
source: title
plugin: callback
callable: mb_strtolower
-
plugin: callback
callable: ucfirst
field_imagen/target_id:
plugin: entity_lookup
value_key: name
source: image
bundle_key: bundle
bundle: image
entity_type: media
ignore_case: 1
access_check: 0Hay muchos plugins disponibles, los puedes ver aquí. Además podemos crear plugins de forma muy fácil.
Ya solo falta indicar a dónde irá todo este contenido con destination. dependerá de la entidad que estemos importando, en este caso son nodos.
destination:
plugin: entity:nodeY aquí todo entero:
id: products
label: Import products
source:
plugin: 'csv'
# Full path to the file.
path: 'modules/custom/custom_migration/data/products.csv'
# Column delimiter. Comma (,) by default.
delimiter: ','
# Field enclosure. Double quotation marks (") by default.
enclosure: '"'
# The row to be used as the CSV header (indexed from 0),
# or null if there is no header row.
header_offset: 0
# The column(s) to use as a key. Each column specified will
# create an index in the migration table and too many columns
# may throw an index size error.
ids:
- code
# Here we identify the columns of interest in the source file.
# Each numeric key is the 0-based index of the column.
# For each column, the key below is the field name assigned to
# the data on import, to be used in field mappings below.
# The label value is a user-friendly string for display by the
# migration UI.
fields:
0:
name: cat
label: 'Categoria'
1:
name: code
label: 'Product code'
2:
name: title
label: 'Title'
3:
name: image
label: 'Image'
process:
field_cat:
plugin: entity_lookup
source: cat
value_key: name
bundle_key: vid
bundle: cat_producto
entity_type: taxonomy_term
ignore_case: true
field_producto_codigo: code
title:
-
source: title
plugin: callback
callable: mb_strtolower
-
plugin: callback
callable: ucfirst
type:
plugin: default_value
default_value: producto
field_imagen/target_id:
plugin: entity_lookup
value_key: name
source: image
bundle_key: bundle
bundle: image
entity_type: media
ignore_case: 1
access_check: 0
destination:
plugin: entity:node
migration_dependencies: {}Ahora finalmente solo nos queda ejecutar los comandos desde drush para realizar la importación. También se puede hacer desde UI en admin/structure/migrate.
Un apunte antes de seguir: voy a usar los aliases cortos porque son los que uso yo en el día a día y los tengo metidos en los dedos. Si prefieres la forma larga, aquí tienes la equivalencia:
| Alias corto | Forma larga |
|---|---|
drush mim | drush migrate:import |
drush mr | drush migrate:rollback |
drush ms | drush migrate:status |
drush mrs | drush migrate:reset-status |
drush cdi | drush config:devel-import |
drush en | drush pm:install |
Lo primero es decirle a drupal que cargue todos los archivos de importación con el módulo config_devel:
drush cdi [nombre_del_modulo]Que en nuestro caso sería:
drush cdi custom_migrateRecuerda que el YAML tiene que estar en config/install y listado en la sección config_devel: del .info.yml. Si falta cualquiera de las dos cosas, drush cdi no lo va a cargar.
Y apunta esto en algún sitio porque vas a tropezar con ello: cada vez que toques el YAML de la migración tienes que volver a ejecutar drush cdi. Si no, los cambios no se aplican y te volverás loco intentando entender por qué la migración sigue haciendo lo mismo de antes. Lo digo por experiencia.
Luego podemos ver las migraciones que tenemos y su estado con:
drush msPara realizar la importación:
drush mim [id_importacion]Que en nuestro caso sería:
drush mim productsSi algo sale mal, que lo normal es que pase, podemos hacer un rollback con este comando:
drush mr productsTambién es posible que la migración se atasque, con lo cual primero antes de hacer el rollback hay que ponerla en idle:
drush mrs productsAlgunos flags que vale la pena conocer
Cuando lleves unas cuantas migraciones a la espalda te encontrarás con que siempre acabas usando los mismos cuatro flags. Los pongo aquí para que no tengas que descubrirlos a hostias como hice yo.
--limit
Para probar la migración con unas pocas filas antes de lanzarla entera. Si tu CSV tiene 50.000 filas y sospechas que algo va a fallar, no te lances de cabeza:
drush mim products --limit=10--update
Reimporta los registros que ya estaban importados. Útil cuando cambias algo en el process y quieres que se aplique también a lo que ya estaba en la base de datos, sin tener que hacer rollback completo:
drush mim products --update--idlist
Para reimportar (o importar por primera vez) un id concreto o varios. Va perfecto cuando estás depurando un caso raro y quieres centrarte en una fila específica:
drush mim products --idlist=ABC123
drush mim products --idlist=ABC123,DEF456--sync
Sincroniza el destino con el origen: importa lo nuevo, actualiza lo existente y borra del destino lo que ya no esté en el origen. Cuidado con este, porque si te equivocas borrando del CSV te cargas contenido en Drupal. Pero si lo que quieres es justo eso, te ahorra mucho lío:
drush mim products --syncY esto es más o menos todo. No pretende ser una guía exhaustiva del módulo migrate, daría para muchísimos posts (importar imágenes a media, paragraphs, taxonomías con jerarquía, plugins custom…), pero como introducción para hacer una migración desde un CSV creo que cubre lo básico.
Si te ha servido y te ha ahorrado un par de horas de cabezazos contra el teclado, pues genial. Y si lo compartes con alguien que esté empezando con migraciones, mejor que mejor.