Datenmigration mit Migrate

Von alt nach Drupal - Migrieren wird zum Kinderspiel

Stellt man eine bestehende Website auf Drupal um möchten die Kunden die alten Inhalte natürlich nicht verlieren. Je nachdem auf welchem System die Website zuvor lief gibt es schon fertige Migration-Tools oder Workarounds im Internet. Aber spätestens wenn es sich um eine Eigenentwicklung, ein unbekanntes CMS oder gar statische HTML-Seiten handelt, stößt man an seine Grenzen. Aber auch von Drupal zu Drupal muss die Datenmigration nicht unbedingt einfach sein. Je nach Datenmenge kann die Migration ziemlich zeitraubend werden. Oft gibt es von dem Kunden dann auch noch die Anforderung, bei dieser Gelegenheit die vorhandenen Inhalte auszumisten und nur bestimmte Inhalte mit auf die neue Seite zu übernehmen. Jetzt gibt es drei Möglichkeiten. Möglichkeit eins, wir entwickeln ein Modul, welches uns die Daten aus dem Quell-CMS ausliest und in das Ziel-CMS hineinschreibt. Möglichkeit zwei, wir suchen uns eine günstige Arbeitskraft, die alle Inhalte per copy ‘n’ paste in das neue System einträgt. Oder Möglichkeit drei, wir setzen auf die Drupal-Community und gucken uns das Migrate-Modul an.

Was macht das Migrate-Modul eigentlich?

Man darf das Migrate-Modul nicht als typisches Drupal-Modul sehen. Es bietet zwar auch ein UI-Modul an, aber man kann sich seine Migrationen nicht per User-Interface zusammenklicken. Grundverständnisse in PHP sind daher von Vorteil. Eine Migration ist ein eigenes Modul, welches die API des Migrate-Moduls verwendet. Die Migration selbst kann man über das Frontend zwar starten, aber wenn möglich sollte man das über Drush machen.

Das Migrate Module kommt der vorgeschlagenen ersten Lösung am nächsten, jedoch brauchen wir uns nur um das “von” und das “nach” kümmern. Also von wo kommen die Daten und als was sollen sie gespeichert werden. Um das speichern der Nodes, Benutzer, Terms, usw. müssen wir uns nicht kümmern, das übernimmt Migrate für uns.

Neben den Grundfunktionalitäten wie dem Speichern bietet das Migrate-Modul aber noch um einiges mehr. Zunächst können wir die Migration natürlich starten, können sie aber auch wieder rückgängig machen. Das ist ziemlich praktisch, falls ein Import nicht erfolgreich war oder ein Fehler aufgetreten ist. Durch einen Rollback werden alle zuvor importierten Daten wieder gelöscht und man hat seinen alten Stand zurück.

Möchte man von einem bekannten CMS zu Drupal wechseln, gibt es schon fertige Migrations für TYPO3, WordPress und phpBB. Möchte man von einer alten Drupal-Version zu einer neuen migrieren gibt es das auch schon fertig in dem Modul D2D.

Ich werde in diesem Artikel von Drupal 6 auf Drupal 7 migrieren.

Eine Migration erstellen

Das vorgehen einer Migration ist immer gleich, zunächst müssen wir uns einen Überblick über die Struktur der Daten machen. Wie werden uns die Daten geliefert? Als CSV, XML oder haben wir zugriff auf die Datenbank? Haben wir uns ein Bild von der Datenstruktur gemacht müssen wir gucken, welche Inhalte überhaupt auf das neue System übernommen werden sollen. In unserem Beispiel wollen wir nur die Blogeinträge übernehmen. Die einzelnen statischen Seiten und Landigpages wollen wir komplett neu gestalten. Zu den Blogeinträgen selbst gehören noch die Benutzer und die Kommentare. Demnach haben wir drei Migrationen. Die Inhaltstypen selbst haben wir aufgeräumt und etwas verändert. Es findet also kein eins-zu-eins Mapping der Felder statt.

Wie zu Anfangs schon gesagt, ist eine Migration zunächst ein eigenes Modul. Wir benötigen also einen eigenen Ordner mit den Drupal-Typischen Modul-Dateien my_module.info und my_module.module. In der Info-Datei geben wir einen Namen, Beschreibung, Package und die Core-Version an. Also nichts neues. Dann sollten wir noch alle Dependencies definieren die wir brauchen, dazu gehören alle Felder die wir während der Migration mit Daten bespielen. Zum schluss noch die Dateien zu unseren eigentlichen Migrationen festlegen. Unsere Info-Datei sieht dann wie folgt aus:

name = "My Migration"
description = "Migration from D6 to D7"
package = "migrate"
core = 7.x

dependencies[] = migrate
dependencies[] = migrate_extras
dependencies[] = field
dependencies[] = file
dependencies[] = image
dependencies[] = number
dependencies[] = text

files[] = migrations/users.inc
files[] = migrations/articles.inc
files[] = migrations/comments.inc

In der Modul-Datei müssen wir standardmäßig nur einen Hook implementieren um dem Migrate-Modul zu sagen welche API wir verwenden möchten und welche Migrationen es gibt. Dabei ist der Array-Key der maschinenlesbare Name der Migration. Darüber wird die Migration angesprochen, also gestartet, zurückgesetzt oder in anderen Migrationen referenziert.

<?php
/**
* Implements hook_migrate_api().
*/
function my_migration_migrate_api() {
 
$api = array(
   
'api' => 2,
   
'migrations' => array(
     
'Articles' => array('class_name' => 'MyMigrationArticles'),
     
'Users' => array('class_name' => 'MyMigrationUsers'),
     
'Comments' => array('class_name' => 'MyMigrationComments'),
    ),
  );

  return
$api;
}
?>

Ich empfehle zusätzlich noch eine Install-Datei, welche unsere definierten Migrationen beim deaktivieren des Moduls deregistriert. Würden wir das nicht machen, wären die Migrationen auch nach dem deaktivieren des Moduls noch bei dem Migrate Modul registriert. Migrate würde also versuchen die Migrationen noch zu finden und auszuführen, aber die Klassen werden nicht mehr geladen, die Folge ist eine Handvoll Fehlermeldungen. Das möchten wir natürlich vermeiden. Also deregistrieren wir die Migrationen im hook_disable:

<?php
function my_migration_disable() {
 
Migration::deregisterMigration('Users');
 
Migration::deregisterMigration('UserProfiles');
 
Migration::deregisterMigration('Articles');
}
?>

Jetzt sieht man schon wo die Reise hingeht. Jede Migration ist eine eigene Klasse. Wenn ich von einer Migration rede meine ich eine Migrations-Klasse. Der Aufbau einer Migration ist immer gleich. Wir definieren eine Quelle (source) - wo die Daten herkommen, ein Ziel (destination) - wo die Daten gespeichert werden sollen, und die zugehörigen Field-Mappings - welches Feld aus der Quelle soll welchem Feld im Ziel zugeordnet werden. Das Migrate-Modul bringt einige Quellen und Ziele von Haus aus mit. Als Quelle können Daten im folgenden Format dienen:

  • MySQL-Datenbank
  • XML
  • CSV
  • JSON
  • Microsoft SQL
  • Oracle

Ist nicht das passende Format dabei kann man natürlich auch seine eigene Quelle definieren. Wie das funktioniert ist in der Dokumentation beschrieben.

Als Ziel können die in Drupal bekannten Objekte dienen:

  • Benutzer
  • Benutzerrole
  • Taxonomy Term
  • Node
  • Kommentar
  • Datei
  • Datenbank-Tabelle
  • Menü

Fehlt einem hier ein Ziel, ist das auch nicht weiter schlimm. In dem Migrate Extras Modul werden noch viele weitere Ziele gesammelt. Eine Übersicht, was noch hinzukommt findet ihr auf der Modulseite.

Die Quelle definieren

Haben wir eine Migrations-Klasse erstellt müssen wir zunächst die Quelle definieren. In unserem Fall gehen wir direkt auf die Datenbank der Drupal 6 installation, demnach eine MigrateSourceSQL-Klasse. Als Argument erwartet die Klasse eine Query, welche die zu migrierenden Nodes aus der Datenbank selektiert. Wir können dazu den Datenbank Abstraktionslayer von Drupal verwenden. Dieser geht allerdings standardmäßig auf die Datenbank der aktuellen Drupal-Installation. Wir möchten natürlich auf die Datenbank der Drupal 6 installation gehen. Dazu müssen wir zunächst eine neue Datenbankverbindung definieren. Das können wir entweder in der Migration selbst machen:

<?php
Database
::addConnectionInfo('for_migration', 'default', array(
 
'driver' => 'mysql',
 
'database' => 'migration_database',
 
'username' => 'username',
 
'password' => 'password',
 
'host' => 'localhost',
 
'prefix' => '',
));
?>

Oder in unserer settings.php

<?php
$databases
['for_migration']['default'] = array(
 
'driver' => 'mysql',
 
'database' => 'migration_database',
 
'username' => 'username',
 
'password' => 'password',
 
'host' => 'localhost',
 
'prefix' => '',
);
?>

Ich würde immer letztere Variante bevorzugen, weil Zugangsdaten in Modulen nichts zu suchen haben. Ist die neue Verbindung definiert können wir die Query wie gewohnt zusammenbauen, natürlich mit der neu erstellten Verbindung:

<?php
$query
= Database::getConnection('default', ‘for_migration')
              ->select('
users', 'u');
?>

Dann fügen wir noch alle benötigten Felder zur Query hinzu und übergeben das ganze Konstrukt an die MigrateSourceSQL-Klasse.

Damit wir die Migrationen zurücksetzen können und innerhalb einer Migration auf zuvor migrierte Daten referenzieren können, brauchen wir ein eindeutiges Feld. Da wir eine Drupal 6 installation haben ist das nicht allzu schwer, da jede Tabelle eine eindeutige ID hat. Im Fall der Benutzer nehmen wir die User-ID (uid):

<?php
$this
->map = new MigrateSQLMap($this->machineName,
  array(
   
'uid' => array(
     
'type' => 'int',
     
'unsigned' => TRUE,
     
'not null' => TRUE,
     
'alias' => 'u',
    ),
  ),
 
MigrateDestinationUser::getKeySchema()
);
?>

Migrate legt dann im Hintergrund eine Tabelle an, in der die alte Benutzer-ID und die neue Benutzer-ID gespeichert werden. Für was das sinnvoll ist kommt später noch.

Wir Mappen jetzt noch die Quell-Felder auf die Ziel-Felder und fertig ist unsere erste Migration. Das Mapping erfolgt immer nach dem gleichen Prinzip:

<?php
$this
->addFieldMapping('source_field', 'destination_field’);
?>

Wir sagen welches Quell-Feld auf welches Ziel-Feld gemappt werden soll. In unserem Fall ist es ziemlich einfaches Mapping, da sich bei den Benutzern von Drupal 6 auf Drupal 7 ziemlich wenig geändert hat, Quell und Zielfeld haben überall den gleichen Namen.

<?php
$this
->addFieldMapping('name', 'name');
$this->addFieldMapping('pass', 'pass');
$this->addFieldMapping('created', 'created');
$this->addFieldMapping('mail', 'mail');
$this->addFieldMapping('roles')
        ->
defaultValue(DRUPAL_AUTHENTICATED_RID);
$this->addFieldMapping('status', 'status');
$this->addFieldMapping('language', 'language');
$this->addFieldMapping('init', 'mail');
?>

Wie man hier schon an den Benutzerrollen sieht, kann man den Mappings wiederum einige Methoden anhängen. Welche es gibt und wofür man sie benutzen kann findet ihr in der Dokumentation.

Das war schon die ganze Zauberei. Wir können die Migration jetzt durchlaufen lassen, zurücksetzen und nochmal einspielen lassen. So oft wir wollen. Bereits vorhandene Benutzer werden nicht noch einmal importiert. Kommen neue Benutzer hinzu, werden diese natürlich noch importiert.

Die restlichen Migrationen sind jetzt eigentlich nur noch Fleißarbeit. Query erstellen, Mappings definieren und importieren. Eigentlich ganz einfach. Auf ein paar Besonderheiten möchte ich aber dennoch eingehen.

Auf eine Migration verweisen

Ich hatte es schon angesprochen, die meisten CM-Systeme sind so aufgebaut, dass die Daten untereinander verknüpft sind. Eine Node hat einen Autor, Kommentare usw. Wenn wir die Daten migrieren, möchten wir diese Referenzen natürlich behalten. Ich nehme als Beispiel wieder die Benutzer-Migration. Im alten System gibt es einen Benutzer mit der ID 5, dieser wird in das neue System importiert und erhält dort die ID 38. Migrate speichert In einer Tabelle die Zuordnung der beiden IDs (sourceid=5, destid=38). Wenn wir dann die Nodes importieren, diese als Autor aber noch auf die alte Benutzer-ID (auf die 5) referenzieren, weiß Migrate dass es im neuen System nicht mehr die 5 sondern die 38 ist und setzt den entsprechenden Wert. Die Nodes werden also dem richtigen Autor zugeordnet. Das ist ein ziemlich praktisches Feature, weil es nicht immer möglich ist die IDs vom alten in das neue System zu übernehmen. Das ganze funktioniert sogar sehr einfach. Zunächst definieren wir in der Migration eine Abhängigkeit zu einer anderen Migration. In unserem Fall sagen wir in der Node-Migration, dass es eine abhängigkeit zu der Benutzer-Migration hat, die Benutzer-Migration also schon erfolgt sein muss, damit die Node-Migration starten kann.

<?php
$this
->dependencies = array(
 
'Users'
);
?>

Dann fügen wir ein ganz normales Field-Mapping für die Benutzer-ID hinzu, hängen aber die Methode sourceMigration an. Diese erwartet als Parameter die Migration, in der die Zuordnung der IDs stattfindet. Also der Benutzer-Migration, denn diese weiß, welche neue ID zu der alten ID gehört.

<?php
$this
->addFieldMapping('uid', 'uid')
            ->
sourceMigration('Users')
            ->
defaultValue(1);
?>

Lassen wir unsere Migration starten, werden wir sehen, dass jede Node seinen Autor behalten hat, obwohl dieser unter Umständen eine völlig andere ID hat. Mit dem gleichen Prinzip migrieren wir die Kommentare zu den Nodes.

Daten vor dem Speichern bearbeiten

Oft kommt es vor, dass die Daten aus dem Quell-System nicht die gewünschte Struktur haben. Oder man möchte die Quell-Daten vor dem Speichern bearbeiten - eine URL austauschen, HTML-Tags entfernen oder was auch immer. Das Migrate Modul bietet dazu gleich zwei Möglichkeiten. Zum einen, wenn die Daten aus der Quelle geholt wurden und bevor sie gemappt werden (prepareRow). Und zum anderen, bevor der Inhalt im Ziel gespeichert wird (prepare).

Möchte man nach dem der Eintrag gespeichert wurde noch etwas machen, kann man dies mit der Methode complete tun. Eine genaue Beschreibung findet ihr wieder in der Dokumentation.

Einen Eintrag überspringen

In unserem Beispiel haben wir alle Artikel zusammen mit den Kommentaren von einer Seite migriert. Allerdings konnte man auf der Seite nicht nur die Artikel kommentieren sondern auch einige andere Inhalte. Da diese aber nicht mit migriert wurden, wäre es schön, wenn wir nur die Kommentare zu den Artikeln migrieren. Man kann einen Eintrag übersprignen, indem man in der Methode prepareRow den Wert “FALSE” zurück gibt. Wir können die Methode überschrieben, prüfen ob die Node-ID des Kommentars importiert wurde, und wenn nicht - dann handelt es sich nicht um einen Artikel und der Kommentar soll nicht importiert werden.

<?php
public function prepareRow($row) {
   
$query = db_select('migrate_map_mymigrationarticles', 'ma');
   
$query->addField('ma', 'sourceid1');
   
$query->condition('ma.sourceid1', $row->nid, '=');

   
$result = $query->execute()->fetchAssoc();

    if(
$result === FALSE) {
      return
FALSE;
    }
  }
?>

Fazit

Auch wenn der Artikel ziemlich lang geworden ist, war das nur ein kleiner Einblick in das Migrate-Modul. Man kann noch viel mehr damit machen. Ein hilfreicher Einstieg ist die wirklich gute Dokumentation und die ganzen Beispielmodule (im Migrate Modul aber auch im Migrate-Extras Modul). Beschäftigt man sich eine Weile mit dem Modul und testet es mit einer überschaubaren Menge an Daten aus, damit man die einzelnen Schritte noch nachvollziehen kann, wird die nächste große Migration zum Kinderspiel.

Nehmen wir etwas Abstand vom typischen Daten-Migrieren, also alte Website zu neuer Website, kann das Migrate Modul uns auch viel Arbeit abnehmen. Bei einigen Zeitungsverlagen ist es beispielsweise üblich, dass die Artikel in XML-Form vorliegen. Mit Hilfe von Migrate kann man dem Kunden eine Möglichkeit bieten die XML-Dateien auf den Server zu laden und die Artikel werden automatisch in die Website eingespielt.

Kommentare

Sven - 26. Februar 2013 - 13:28
Hallo zusammen, Vorsicht: im obigen Beispiel sind die Source- und Destination-Parameter im addFieldMapping vertauscht. Richtig herum lautet der Aufruf "addFieldMapping($destination_field, $source_field = NULL, $warn_on_override = TRUE);" Gruß, Sven
Helrunar - 31. Januar 2013 - 9:29
für die Migration eines PHPBB3 in das Forum von Drupal gibt es ein Sandboxprojekt http://drupal.org/sandbox/blackice2999/1240812 und für die Migration von Joomla 1.0 und 1.5 funktioniert dieses Modul hier sehr gut http://drupal.org/project/joomla Ob dieses allerdings auch bereits für eine Migration von J2.5 zu Drupal verwendet werden kann, kann ich nicht sagen.