Drupal → Модуль Migrate — миграция данных в Drupal из любых источников

18.12.2011

Описание

Модуль Migrate это фреймворк для миграции (импорта) данных в Drupal из любых источников. Например с помощью этого модуля можно перенести контент из сторонней CMS, или просто сделать миграцию с одной ветки друпала на другую (например 5 → 7).

Из коробки модуль умеет импортировать данные в:

  • ноды
  • пользователей
  • комментарии
  • термины таксономии
  • блоки
  • любые сущности
  • файлы
  • меню и пункты меню
  • переменные
  • произвольные таблицы бд

При необходимости можно написать свой destination плагин.

Основные отличия от других способов миграции (Feeds, Node Export/Node Import и т.д.):

  • Полный контроль над процессом миграции и над данными.
  • Любой источник данных (теоретически). Из коробки это XML, CSV, XLS, JSON, IBM DB2, MongoDB, MSSQL, Oracle, файлы.
  • Поддержка всех Drupal 7 сущностей и полей.
  • Возможность сделать rollback после импорта, т.е. отменить все изменения.
  • Статистика по каждой миграции.
  • Зависимые миграции.
  • Интеграция с Drush.

Использование

Для начала переноса контента нужно создать модуль, реализовать в нём хук hook_migrate_api() и написать классы миграции, расширяющие Migration.

Одна миграция может импортировать только один тип данных (один bundle в терминологии Drupal 7), например ноды определённого типа или термины таксономии определённого словаря.

Для примера создадим модуль example_migration, который будет заниматься миграцией данных из mysql таблицы pages в ноды типа page.

1. Реализация hook_migrate_api()

Добавляем в файл example_migration.migrate.inc:

/**
 * Implement hook_migrate_api()
 */
function example_migration_migrate_api() {
  return array(
    'api' => 2,
    'groups' => array(
      'example' => array(
        'title' => 'Example',
      ),
    ),
    'migrations' => array(
      'Pages' => array(
        'class_name' => 'PagesMigration',
        'group_name' => 'example',
      ),
    ),
  );
}

В хуке указывается версия api, название группы и список ваших миграций. Подробнее.

2. Реализация класса миграции

Создаём файл PagesMigration.inc и добавляем:

class PagesMigration extends Migration {
  public function __construct($arguments) {
    parent::__construct($arguments);
  }
}

Добавляем в example_migration.info информацию о файле, в котором реализован класс:

files[] = PagesMigration.inc

Сбрасываем кэш.

3. Описание источника данных

В методе __construct, с помощью Database API, делаем выборку из источника и заполняем переменную $this->source:

$query = db_select('pages', 'p')->fields('p', array('pgid', 'page_title', 'page_body'));
$this->source = new MigrateSourceSQL($query);

В примере подразумевается что таблица pages находится в одной бд с друпалом. Если это не так, то код немного меняется:

$query = Database::getConnection('default', 'otherdb')->select('pages', 'p')->fields('p', array('pgid', 'page_title', 'page_body'));
$this->source = new MigrateSourceSQL($query, array(), NULL, array('map_joinable' => FALSE));

4. Описание цели миграции

В методе __construct заполняем переменную $this->destination:

$this->destination = new MigrateDestinationNode('page');

В качестве параметра MigrateDestinationNode() указывается машинное имя материала.

5. Описание ключевых полей источника и цели

Модуль Migrate поддерживает откат изменений и обновление уже импортированных данных. Для правильной работы этих функций нужно в методе __construct заполнить переменную $this->map информацией о первичных ключах источника и цели:

$source_key_schema = array(
  'pgid' => array(
    'type' => 'int',
    'unsigned' => TRUE,
    'not null' => TRUE,
  )
);
$this->map = new MigrateSQLMap($this->machineName, $source_key_schema, MigrateDestinationNode::getKeySchema());

Вызов метода MigrateDestinationNode::getKeySchema() равносилен коду:

array(
  'nid' => array(
     'type' => 'int',
     'unsigned' => TRUE,
     'description' => 'ID of destination node',
  ),
)

6. Маппинг полей

В методе __construct, с помощью Migrate::addFieldMapping(), делаем маппинг полей:

$this->addFieldMapping('title', 'page_title');
$this->addFieldMapping('body', 'page_body');

Первый параметр — это поле цели, второй — поле источника.

Если включить модуль Migrate UI и на странице admin/content/migrate кликнуть по имени миграции, то можно будет посмотреть советы по маппингу:

Итого

Полный листинг класса миграции:

class PagesMigration extends Migration {
  public function __construct($arguments) {
    parent::__construct($arguments);

    // Source
    $query = db_select('pages', 'p')->fields('p', array('pgid', 'page_title', 'page_body'));
    $this->source = new MigrateSourceSQL($query);

    // Destination
    $this->destination = new MigrateDestinationNode('page');

    // Key schema
    $source_key_schema = array(
      'pgid' => array(
        'type' => 'int',
        'unsigned' => TRUE,
        'not null' => TRUE,
      )
    );
    $this->map = new MigrateSQLMap($this->machineName, $source_key_schema, MigrateDestinationNode::getKeySchema());

    // Mapping
    $this->addFieldMapping('title', 'page_title');
    $this->addFieldMapping('body', 'page_body');
  }
}

После написания миграции идём на страницу admin/content/migrate, переходим в группу Example, отмечаем нужную миграцию, выбираем операцию Import и жмём Execute:

Полезные ссылки

Пример миграции данных из Drupal 6 в Drupal 7
Пример миграции данных из Drupal 5 в Drupal 7
Пример миграции данных из PHPBB3 в Drupal 7
Официальная документация
Content migration using the Migrate module (видео)
Импорт данных с фреймворком Migrate (доклад Владислава Богатырева)
Импорт данных в Drupal - легко и просто (доклад Дмитро Данилевский)
Мои посты про Migrate

Написанное актуально для
Migrate 7.x-2.8
Похожие записи

Комментарии

Гость
21.12.2011, 20:43

в Вашем блоге всегда можно найти что-то интересное и новое для себя, спасибо)

не подскажете как проще доработать рабочий сайт, не переводя его в режим обслуживания на период обновления/доработки?
полагаю данный модуль может помочь в данном вопросе, провести все необходимые работы над сайтом а затем с "живого" сайта перекинуть все материалы на новую версию...

Вы не сталкивались с этим, возможно подскажете, как лучше провести обновления?

провести все необходимые работы над сайтом а затем с "живого" сайта перекинуть все материалы на новую версию...

именно так. сам процесс миграции занимает считанные минуты

Полный контроль на процессом миграции и над данными.
Любой источник данных (теоретически). Из коробки это БД, XML, JSON, CSV.
Поддержка всех Drupal 7 сущностей и полей.
Возможность сделать rollback после импорта, т.е. отменить все изменения.
Статистика по каждой миграции.
Зависимые миграции.
Интеграция с Drush.

Не совсем уверен на счёт статистики по каждой миграции и зависимых миграций. Но всё остальное в Feeds есть, за исключением интеграции с Drush, которая уже на подходе.

как в feeds скопировать файлы из одного места в другое?

как быть если нет возможности сохранить primary key? т.е импортировать юзеров без сохранения pk, а потом ноды?

как откатывать импорты?

как импортировать сложные поля вроде address, date или кастомного составного поля?

как внедрить свои настройки в импорт?

как в одни клик сделать миграцию всех данных (юзеры, ноды, термины)?

и главный вопрос как это всё отлаживать?

как в feeds скопировать файлы из одного места в другое

feeds умеет закачивать файлы, т.е. к примеру отправить картинку в field_image можно без проблем. Если что то не стандартное, то через собственный плагин.

как быть если нет возможности сохранить primary key? т.е импортировать юзеров без сохранения pk, а потом ноды?

не знаю что имеется в виду, НИДы для ноды тоже можно задавать при импорте

как откатывать импорты?

Есть такая возможность. Хотя, точно не помню, потому что ни разу не пользовался. Обычно полагаюсь на стандартный бекап сайта.

как импортировать сложные поля вроде address, date или кастомного составного поля?

date и addressfield имеют интеграцию с Feeds поэтому можно с ними работать так же как и с остальными полями. Для собственных составных полей можно сделать плагин-маппер.

как внедрить свои настройки в импорт

Есть UI с кучей настроек, можно расширить его через API

и главный вопрос как это всё отлаживать?

Как обычно.

не знаю что имеется в виду, НИДы для ноды тоже можно задавать при импорте

если импортировать юзеров без сохранения pk, то не понятно как потом импортировать ноды, где uid уже будет не актуальным. в Migrate для этого есть зависимости миграций.

Как обычно.

т.е. никак :)

Врать не буду, последний раз Фидс запускал почти год назад. Насколько помню он при импорте сохраняет PK каждой исходной записи в своей собственной таблице, с тем чтобы можно было их связывать при следующем импорте или просто чтобы откатится при необходимости.

т.е. никак :)

А что в Майгрэйте какая то особенная отладка есть?

ну если учесть что миграция это класс, в котором можно наставить брикпоинтов на любую стадию импорта, то по сравнению с feeds, да, особенная :)

для меня feeds это чёрный ящик в который приходят данные, он что-то там крутит и выдаёт ноду. в migrate же всё прозрачно.

А можно пример как отправить картинку в field_image ?

$this->addFieldMapping('field_image', 'img_path')->arguments(array(
  'file_function' => 'file_copy',
  'file_replace' => FILE_EXISTS_RENAME,
));

модуль состоит из 2х файлов?
example_migration.info
example_migration.module
в example_migration.module вставляется хук и класс?

на странице admin/content/migrate после включения модуля ничего не появляется

кстати тут http://drupal.org/node/1006982 как я понял, теперь надо в inc файле объявлять класс в версии Migrate 2.5 и выше. может в этом проблема?

Модуль Migrate поддерживает откат изменений и обновление уже импортированных данных.

Тут сообщают про новую модную штучку для обновления в Migrate: track_changes
Начиная с версии 2.6 Migrate умеет сам определять, изменился ли контент в сорсе с момента последнего запуска импорта.

great.earl
19.06.2013, 11:19

Вопрос - почему своя миграция не появляется в списке /admin/content/migration?
Друпал 7, модуль свой написал, хуки есть, цель импорт из CSV в свою таблицу MySQL.

function csvtotable_migrate_api() {
  $api = array(
    'api' => 2,
  );
  return $api;

class csvtotable extends Migration {
  public function __construct() {
    parent::__construct();
  
    $options = array(); $options['header_rows'] = 1; 
    $options['delimiter'] = "\t"; 
    $columns[0] = array('t_id', 'ID'); 
    $columns[1] = array('t_text', 'Text String'); 
    $columns[2] = array('t_num', 'Number row'); 
    
    $this->source = new MigrateSourceCSV('myfile.csv', $columns, $options);
    $this->destination = new MigrateDestinationTable('CustomTable');

    $table_name = 'CustomTable';
    $this->map = new MigrateSQLMap($this->machineName,
      array('t_id' => array(
              'type' => 'int',
              'unsigned' => TRUE,
              'not null' => TRUE,
             )
           ),
        MigrateDestinationTable::getKeySchema($table_name)
      );
    
    // Mapping
    $this->addFieldMapping('id', 't_id');
    $this->addFieldMapping('tableNumber', 't_num');
    $this->addFieldMapping('tableString', 't_text');
  }
}

может что-то не так пишу?
Спасибо.

класс нужно зарегистрировать в .info файле

great.earl
19.06.2013, 15:50

так есть это

name = CSV to DB Table
description = Allows to import CSV-files to custom MySQL table.
version = "7.x-1.0"
core = "7.x"
package = "CustomAddedModules"
dependencies[] = migrate
files[] = csvtotable.module

почитайте маны, там что-то менялось в новых версиях

great.earl
20.06.2013, 09:35

описал класс в файле csvtotable.migrate.inc, прицепил его, в .module остался только хук. теперь импортер появляется в списке migrate, но при импорте валится с ошибками.
Undefined property: stdClass::$id File
Undefined property: stdClass::$TableNumber
Undefined property: stdClass::$TableString
Column not found: 1054 Unknown column 'map.sourceid2' in 'where clause'

Не подскажете, чем может быть вызвано, что-то не так описал в коде?

great.earl
20.06.2013, 09:36

TableString TableNumber id - это поля в таблице.

great.earl
20.06.2013, 10:58

Разобрался и все настроил. Выкладываю ниже код класса, который надо прописать в файле имя_модуля.migrate.inc (.inc не забываем прописать в .info, в .module оставляем только хук) для импорта данных из CSV в таблицу БД MySQL. Думаю, втор блога не будет против)))

class csvtotable extends Migration {   //Импорт данных из CSV-файла в пользовательскую таблицу MySQL
  public function __construct() {
    parent::__construct();
 //Source  
   $this->description = t('Test CSV'); //Описание своего импортера, появится в admin/content/migrate
   $columns = array(
    array('id', 'tid'),   // поля в CSV-файле, у меня пока всего 3 'id' - поле, 'tid' - описание поля,  появится в admin/content/migrate
    array('text', 'ttext'),
    array('num', 'tnum'),
  );
 
  $this->source = new MigrateSourceCSV('/tmp/customtest.csv', $columns); // путь откуда импортер берет CSV-файл
 
    $table_name = 'csvtable'; // имя таблицы, куда грузим данные. Создать руками в phpmyadmin или  модуль Data
    $this->map = new MigrateSQLMap($this->machineName, // связка по id - PK
      array('id' => array(
              'type' => 'int',
              'unsigned' => TRUE,
              'not null' => TRUE,
             ) 
           ),
        MigrateDestinationTable::getKeySchema($table_name)
      );
  // Destination  
    $this->destination = new MigrateDestinationTable($table_name);
  // Mapping   
    $this->addFieldMapping('id', 'id');
    $this->addFieldMapping('f_text', 'text');
    $this->addFieldMapping('f_num', 'num');
}
Дмитрий
08.07.2013, 22:17

Здравствуйте. У меня есть задача сделать обновление нод на сайте. То есть есть поле "код", надо по этому полю обновлять поле "Цена". пишу миграцию по Вашему примеру, не могли Вы проверить где у меня ошибка? Вот код

 <?php
/**
 * Implement hook_migrate_api()
 */
function csv_migration_migrate_api() {
  return array('api' => 2);
}

class NodePageMigration extends Migration {
  public function __construct() {
    parent::__construct();
 
    // Source
    $columns = array(
      0 => array('id', 'id'),
      1 => array('price', 'price'),      
    );
    $this->source = new MigrateSourceCSV('/sites/default/files/feeds/update.csv', $columns);
 
    // Destination
    $this->destination = new MigrateDestinationNode('product');
 
    // Key schema
    $source_key_schema = array(
      'pgid' => array(
        'type' => 'int',
        'unsigned' => TRUE,
        'not null' => TRUE,
      )
    );
    $this->map = new MigrateSQLMap($this->machineName, $source_key_schema, MigrateDestinationNode::getKeySchema());

    // Mapping
    $this->addFieldMapping('id', 'id');
    $this->addFieldMapping('price', 'price');
  }
}

Спасибо.

Дмитрий
08.07.2013, 23:21
Migration failed with source plugin exception: fopen  (http://www.rpr1990.ru/sites/default/files/feeds/update.csv) [function.fopen]: failed to open stream: HTTP request failed! HTTP/1.1 401 Authorization Required File /home/u354920/rpr1990.ru/www/sites/all/modules/migrate/plugins/sources/csv.inc, line 173

А с чем может быть связана вот эта ошибка? Файл на сервере есть, по ссылке открывает.

Дмитрий
08.07.2013, 23:43

Сейчас еще вылезла вот такая ошибка

 Warning: file(/sites/default/files/feeds/update.csv) [function.file]: failed to open stream: No such file or directory в функции MigrateSourceCSV->computeCount() (строка 155 в файле /home/u354920/rpr1990.ru/www/sites/all/modules/migrate/plugins/sources/csv.inc).

может это из-за нее моя миграция не работает?

great.earl
09.07.2013, 07:20

решил свою задачу выше с помощью модулей feeds и data - удобнее, в части интерфейса для пользователя, именно импорт из CSV в таблицы MySQL.

в конструкторе MigrateSourceCSV нужно указывать полный путь к файлу в файловой системе. не думаю, что у вас он лежит в /sites/default/files/feeds/update.csv

Дмитрий
09.07.2013, 10:38

Все я разобрался с этим. Теперь осталось написать правильный обработчик ключевого поля в файле. Не подскажете? А то я все смотрел с d.org, а там про ключи найти не могу. Все дело в том что я хочу обновлять значение поля "цена", по значению поля "код товара". Пока что код для ключевого поля вот такой

// Key schema
    $source_key_schema = array(
      'field_product_code' => array(
        'type' => 'text',
        'unsigned' => TRUE,
        'not null' => TRUE,
      )
    );
    $this->map = new MigrateSQLMap($this->machineName, $source_key_schema, MigrateDestinationNode::getKeySchema());

Я подправил все предыдущие ошибки и теперь после импорта просто создает новые ноды, а не обновляет уже существующие.
Еще вот такой вопрос http://clip2net.com/s/5mkCU0 . Можете объяснить?

Дмитрий
09.07.2013, 10:45

решил свою задачу выше с помощью модулей feeds и data - удобнее, в части интерфейса для пользователя, именно импорт из CSV в таблицы MySQL.

я feeds использую просто для импорта нод. но у меня условие от зака обновлять по коду, так как в 1С не все названия совпадают с заголовками на сайте. На сайте около 15к нод, около 6к не совпадают в заголовке.
В feeds нет возможности установить любое ключевым - только url, guid, title. Пробовал ставить модуль Unique Field - не помогло.

Дмитрий
10.07.2013, 12:04

Пасиб, попробую вашим способом

Mneznakomec
21.10.2013, 14:49

в конструкторе MigrateSourceCSV нужно указывать полный путь к файлу в файловой системе.

$pathSourceFile = file_default_scheme() . '://feeds/blog.csv';

Уже долгое время бьюсь с этим модулем, делал все как в статье... не получается... может есть какой-то полный пример решения задачи?!

Да, там был... пытался разобраться, но видимо моих знаний недостаточно дабы постичь в одиночку ;)

Гость
09.12.2014, 23:33

Добрый вечер, посоветуйте способ или модуль для переноса нод из друпал 7 на друпал 6

Игорь
18.04.2016, 15:39

Скажите, удастся ли экспортировать помимо типов материалов еще и их настройки отображения?

Если напишите свой MigrateDestination, то удастся что угодно.

lexsaenko
05.06.2016, 14:43

Здравствуйте, помогите с такой ситуацией. Переезжаю на Drupal 8. Миграция прошла успешно. Отключил модули Migrate Migrate Drupal Migrate Drupal UI (больше не нужны). Но в базе данных осталось много таблиц migrate_map_... . Можно ли их удалить? И почему они не удалились автоматически?

Серега
11.06.2017, 19:49

Уважаемый xandeadx!
У меня ситуация такая:
Имеется xml

<realty-feed xmlns="http://webmaster.yandex.ru/schemas/feed/realty/2010-06">
    <generation-date>2017-02-09T09:18:17+04:00</generation-date>
    <offer internal-id="22960">
        <type>send</type>
        <property-type>living</property-type>
        <category>townhouse</category>
    </offer>
........

Если убрать атрибут xmlns, то импорт проходит отлично. Если оставить, то ноды создаются пустые. Ошибок нет никаких.

Я так понимаю что xmlns - пространство имён.
Пробовал делать так:

$namespaces = array('http://webmaster.yandex.ru/schemas/feed/realty/2010-06');
$this->source = new MigrateSourceXML($items_url, $item_xpath, $item_ID_xpath, $fields, array(), $namespaces);

Не помогает. Вы не в курсе как это исправить?

Задал вопрос на drupal.org, ответов нет (

Игорь
10.01.2022, 17:00

Может есть какая новая информация на сегодняшний день по миграции с 7 на 9?
Один сайт из 60 страниц ручками переношу, а вот второй конечно хотелось бы упростить.

Добавить комментарий