Migrate-module (Drupal)

Uit De Vliegende Brigade
Ga naar: navigatie, zoeken

De Migrate-module doet imports en kan overweg met diverse soorten externe databronnen. De grote kracht van Migrate boven Feeds, is dat Migrate ingewikkelde mappings kan doen. Je kunt PHP en SQL gebruiken bij het configureren van importttaken. De documentatie startpagina zegt zelfs: if you do not have object-oriented programming skills in PHP, this module is not for you. Gelukkig is de werkelijkheid iets minder dramatisch: Diverse van dit soort programmeersels zijn beschikbaar als submodules voor Migrate. Deze module kent uitstekende drush-ondersteuning. Gebruik via Drush is veel sneller dan gebruik via de webinterface.

Dit artikel behandelt ook functionaliteiten van Migrate Extras, waar dat zo uitkomt.

Inleiding

De Migrate-module is een module voor complexe importprocedures. Een migrate-project programmeer je in php als een submodule van de Migrate-module.

Onderdelen

De module bestaat uit de volgende onderdelen:

  1. Een API voor defineren en beheren van migratieprocessen
  2. Een verzameling drush-commando's
  3. Webinterface
  4. Een voorbeelden-module (migrate_example).

Functionaliteiten

  1. De mogelijkheid om bestaande functionaliteit uit te breiden of aan te passen
  2. Ondersteuning voor PDO (PHP Data Objects, DBTNG), XML, CSV, JSON, MySQL, MSSQL, Oracle API, plus uitbreidbaar naar andere soorten bronnen
  3. Ingebouwde ondersteuning voor nodes, users, taxonomieën, comments en bestanden. Uitbreidbaar naar andere objecten
  4. Mapping-tables voor de correspondentie tussen bron- en doelobjecten
  5. Roll-back-mogelijkheid, zodat je gemakkelijk middels trial-&-error dingen kunt uitproberen
  6. Tools voor beheer van afhankelijkheden tussen objecten
  7. Automatisch geheugenbeheer
  8. Framework for logging
  9. Web-UI geschikt voor zowel ontwikkelaars als behanghebbenden.

Aan de slag

  • Wellicht het beste voorbeeld om mee te beginnen: Bar-bones example
  • De submodule migrate_example bevat een aantal voorbeelden, te beginnen met beer. Waarschijnlijk ook handig (voor mij althans): De CMU-module reverse engineeren
  • Als je een klasse hebt gedefineerd, niet vergeten om 'm te registreren [1]

Waar is de webinterface voor?

De essentie van Migrate zit in php-bestanden. Waar is de webinterface dan voor? Nou:

  1. Dashboard: Het dashboard biedt een overzicht van lopende processen, groepen, taken en handlers
  2. Bediening: Mensen die geen gebruik kunnen maken van Drush, kunnen via de webinterface de module starten en stoppen
  3. Diverse instellingen: Een hoop dingen kun je via de webinterface instellen, waaronder overrulen van mappings en default-waardes. Ik denk dat dat verrekte handig is.
  4. Instellingen submodules: Dit is bij uitstek de plek voor configuratiepagina's van contributed modules, zoals de database-specificatie voor migrate_d2d.

Drush

Migrate heeft z'n eigen set Drush-commando's, die welliswaar niet kritiek zijn, maar het leven wél opmerkelijk veraangenamen, en migratie-processen flink versnellen. Zie aparte hoofdstuk elders in dit artikel.

Architectuur

Migraties worden gerealiseerd door migratietaken. Deze bevinden zich in afzonderlijke submodules van de Migrate-module.

Migratie-klasses

Programmeren van een migratietaak gebeurt in php en is sterk object-georiënteerd. Het begint met de instantiatie van een afgeleide klasse van de centrale migration-klasse. In deze afgeleide klasse incorporeer je instanties van vier andere klassen, die hieronder afzonderlijk worden behandeld:

  1. Source classes - MigrateSource
  2. Destination classes - MigrateDestination
  3. Map classes - MigrateMap
  4. Field mappings - MigrateFieldMapping.

Naast deze vier migratie-klasses heb je nog diverse aanvullende entiteiten, waaronder handler classes. Zie aldaar.

Source classes

De MigrateSource-klasse zorgt voor representatie van de brondata. De Migration-klasse (de hoofdklasse) itereert door de rijen die door deze klasse worden gegenereert. Dit zijn de source classes [2]:

  • MigrateSourceSQL: Indien de bron een database is die door Drupal benaderd kan worden. Voor details: [3]
  • MigrateSourceList: Indien data uit verschillende bronnen bij elkaar komt, gebruik dan de MigrateSourceList-bronklasse. Zie [4] voor meer.
  • MigrateSourceMultiItems: Voor representatie van complexe data: [5]
  • MigrateSourceXML Import van XML-based sources: [6]
  • MigrateCourseCSV: Import van CSV-data: [7]
  • MigrateListJSON & MigrateItemJSON Voor import van JSON-gebaseerde data: [8]
  • MigrateSourceMSSQL: Data uit MS SQL Server peuteren? Dat kan!
  • MigrateSourceOracle: Of liever data uit Oracle halen Ook geen probleem.
  • MigrateListFile & MigrateItemFile: Toch liever data uit HTML of andersoortige bronnen halen? Ook dat kan.
  • MigrateSourceMongoDB: Data mag ook uit MongoDB komen.
  • Eigen bronklasse: En mochten de klasses hiervoor niet zijn wat je zoekt, dan kun je altijd nog je eigen klasse defineren.

Destination Classes

De MigrateDestination-klasse en diens subklasse MigrateDestinationEntity Representeren het doelobject van een migratie. De MigrateDestination-klasses roepen daarnaast handlers aan uit subklasses van MigrateDestinationHandler voor hand- en spandiensten. Klasses:

De Migrate Extras-module doet hier een duit in het zakje in de vorm van de MigrateDestinationEntityAPI-klasse.

Map classes

De MigrateMap-klasse houdt de correspondenties bij tussen gemigreerde objecten. Da's reuzehandig voor bv. rollback, of het probleem van veranderde ID's (elders behandeld). Wat de documentatie over map classes zegt:

A key feature of the Migrate module is the tracking of relationships between
source records and the Drupal objects that have been created from them. This
tracking is managed by the map classes - the abstract class MigrateMap and the
concrete class MigrateSQLMap.

MigrateSQLMap maintains two tables for each defined migration - a map table
and a message table. For each source record that is processed by the import
function (whether or not it is successfully imported), a row in the
migration's map table is added, keyed by the source record's unique ID. If
import of the record was successful, the unique ID of the resulting Drupal
object is also stored there. Thus, we know what Drupal objects have been
created via migration (thus which ones can be rolled back), and when updating
imported content we know what source records to update it from. And,
critically, when migrating relationships between objects, we can use the map
table to rewrite those relationships so they are properly maintained on the
Drupal side.

In de praktijk heb je met map classes alleen te maken bij het aanroepen van de migration constructor aan het begin van de migratietaak. Aspecten:

  • Machine name van de migratietaak
  • Source key schema definition: Defineer het veld of set van velden dat gebruikt kan worden om individuele bronregels te onderscheiden
  • Destination key schema definition: In tegenstelling tot de bron, is het schema van het doel bekend. Deze kun je boven water toveren met getKeySchema(), maar ik denk dat het ook handmatig kan
  • Connection key
  • Options array

Voorbeeld product.inc, onderdeel van de CMU-module:

// 1. MigrateSQLMap
//////////////////////////////////////////
//
$this->map = new MigrateSQLMap($this->machineName,
   array(
      'nid' => array(
         'type' => 'int',
         'unsigned' => TRUE,
         'not null' => TRUE,
         'description' => 'Ubercart node ID',
         'alias' => 'ucp',
       ),
   ),

   // 2. MigrateDestinationEntityAPI
   //////////////////////////////////////////
   //
   MigrateDestinationEntityAPI::getKeySchema('commerce_product')
);

Field Mapping

MigrateFieldMapping zorgt voor de mapping van een bronveld naar een doelveld [9]. Een MigrateFieldMapping-instantie bevat meestal een aantal mappings. Enkele functionaliteiten:

  • Recht-toe-recht-aan-mapping
  • Conversie van strings naar arrays
  • Subfields (bv. afbeeldingen of body)
  • sourceMigration: Behouden van de relatie tussen objecten, ook als na migratie de ID's zijn veranderd. Bv. dat Drupal Commerce-orders nog steeds zijn geassociëerd met de juiste accounts, ook al hebben die accounts niet meer de oorspronkelijke UID's
  • Deduplication. Bv. import van productern in Drupal Commerce, afkomstig uit Übercart. In deze laatste mogen SKU's hergebruikt worden. In Drupal Commerce niet
  • Documenting mappings: Voorzieningen om een zelf-documentered systeem te ontwikkelen.

Aanvullende exotica

Naast bovengenoemde vier klasses, is er nog een ruim assortiment exotica.

Handler classes

Een voorbeeld van zo'n aanvullende klasse, betreft handler classes. Wat de handleiding over handler classes zegt:

Handlers are classes which enable you to add additional behavior to the
processing of data being imported. These are usually used to support
migration into contributed or custom modules that maintain their own data
connected to core entities. Most of the time this will be related to custom
field types.

Vermoedelijke voorbeelden van objecten waarvoor je handler classess nodig hebt:

  • Drupal Commerce product-types
  • Drupal Commerce displays
  • Drupal Commerce orders
  • Entity-relationship-veld.

Aanvullende methodes

Verder zijn de volgende methodes beschikbaar [10]:

  • prepareRow($row)
  • prepare($entity, stdClass $row)
  • complete($entity, stdClass $row)
  • createStub(Migration $migration, array $source_id)

Nog exotischer

En als dat je nog niet exotisch genoeg is, kun je altijd nog je toevlucht nemen tot [11]:

  • pre/post Import/Rollback methods
  • pre/post Rollback methods per Entity
  • prepareKey method.

Drush

Migrate komt met een eigen set van Drush-commando's, die prettig in het gebruik zijn, en migratie-processen flink versnellen.

Waarom migreren via Drush?

Het is doorgaans beter om een import via Drush te doen dan via de webinterface [12]:

  • De webinterface gaat uit van een time-out van 25s, dus elke 25s wordt het importproces gestopt en opnieuw gestart. Vooral bij grote migraties geeft dat veel overhead
  • Sneller
  • Betere debugging-info
  • Robuuster want minder points-of-failures.

Alle commando's

migrate-analyze (maz) 	           Analyze the source fields for a migration.
migrate-audit (ma) 	           View information on problems in a migration.
migrate-register (mreg)            Register or re-register any statically defined migrations.
migrate-deregister 	           Remove all tracking of a migration
migrate-fields-destination (mfd)   List the fields available for mapping in a destination.
migrate-fields-source (mfs) 	   List the fields available for mapping from a source.
migrate-import (mi) 	           Perform one or more migration processes
migrate-mappings (mm) 	           View information on all field mappings in a migration.
migrate-messages (mmsg) 	   View any messages associated with a migration.
migrate-reset-status (mrs) 	   Reset a active migration's status to idle
migrate-rollback (mr) 	           Roll back the destination objects from a given migration
migrate-status (ms) 	           List all migrations with current status.
migrate-stop (mst) 	           Stop an active migration operation
migrate-wipe (mw) 	           Delete all nodes from specified content types.

Voorbeelden

drush mi CommerceMigrateUbercartProductProduct
drush mi b755a3db1User
drush mrt CommerceMigrateUbercartCustomerBillingProfile   # Stop
drush mrs CommerceMigrateUbercartCustomerBillingProfile   # Reset: Vaak nodig bij storingen

Migratietaken & webinterface: Voorbeeld

Migrate » Dashboard: Een verse installatie van Migrate levert onder Content twee tabbladen op: Dashboard en Configuration. Onder Dashboard vind je taken & groepen en een control om het proces te starten, stoppen, terug te draaien, etc. Verder nog de groep Options met twee kleine configuratiemogelijkheden
Migrate » Configuration: Registratie van statisch gedefineerde klasses, wat kleine dingen, en het vlak Helpers

Migrate » Configuration » Helpers

Zoals eerder al genoemd, zijn helpers klasses om nieuwe doelobjecten of -velden te defineren. Het is niet ondenkbaar dat zo'n object meerdere keren gedefineerd wordt. Bv. in twee contributed modules. Op deze pagina kun je conflicterende helpers uitschakelen.

Migrate » Configuration » Helpers: Overzicht van handlers

Migrate Extras

Helpers na installatie Migrate Extras: Na installatie van de module Migrate Extras is het overzicht van helpers uitgebreid met een aantal items
Bestanden module Migrate Extras: Qua bestanden is Migrates extras weinig opwindend: Per handler een .inc-bestand
Code geofield.inc: Inhoud van geofield.inc, inclusief klasse-definitie, constructor en een publieke functie

Commerce Übercart Migrate (CUM)

Na installatie van CUM, is het dashboard ongewijzigd, maar is er wel een sub-tabblad Configure Ubercart Migration bijgekomen
Sub-tabblad Configure Ubercart Migration:, met de gegevens van de additionele database die is geconfigureerd in settings.php
Migration group: Na configuratie van de additionele database + registreren van statische klasses, verschijnt deze group
Taken: De verschillende taken binnen de Commerce Migration Ubercart-groep

Anatomie van een taak

Ter illustratie de taak CommerceMigrateUbercartOrder in detail, onderdeel van de CMU-module:

Taak CommerceMigrateUbercartOrder » Overview: Een korte omschrijving van de betreffende taak
Taak CommerceMigrateUbercartOrder » Destination: Unmapped betreft waarschijnlijk velden die voorkomen in destination, maar waar geen bron voor gespecificeerd is
Taak CommerceMigrateUbercartOrder » Source: Specificatie van de bronvelden en -query. Ik denk dat machine name voor mij hetzelfde is als database-naam
Taak CommerceMigrateUbercartOrder » Mapping: Done Ook aardig: Overzichtje van wat wel goed gaat
Taak CommerceMigrateUbercartOrder » Edit: Er is maar één Edit-scherm, en dit is het eerste gedeelte van dat scherm. Hier kun je standaardwaardes en mappings bepalen, (maar geen complexere bewerkingen)
Taak CommerceMigrateUbercartOrder » Edit: Onderste gedeelte van het scherm.

Rollback

Om een taak opnieuw uit te voeren, zul je vrij snel gebruik moeten maken van rollback:

  • Als er '0' staat onder Unprocessed, weigert Migrate de betreffende taak opnieuw uit te voeren
  • Ook als je de betreffende data al op een andere manier hebt verwijderd, moet je tóch rollback doen.

Voorbeeld:

drush mr CommerceMigrateUbercartOrder   # mr = migrate rollback
drush mi CommerceMigrateUbercartOrder   # mi = migrate import

MigrateDestinationEntityAPI::getKeySchema

Afbeelding-gerelateerde doelvelden binnen CommerceMigrateUbercartNodeProduct, boven water getoverd dankij MigrateDestinationEntityAPI::getKeySchema

Migrate Extras heeft een API-call genaamd

MigrateDestinationEntityAPI::getKeySchema

die reuzehandig is om het complete doelschema van bv. een Drupal Commerce producttype te regelen. Maar soms is-ie niet zo reuzehandig. Vandaar dit hoofdstuk.

Alternatieve producttypes specificeren?

Deze methode wordt gebruikt in de code van taak CommerceMigrateUbercartProductMigration van de CMU-module. Deze fetchet het schema van commerce_product(product), terwijl ik vermoedelijk commerce_product(koolborstel) nodig heb. Hoe pas ik dat aan?

Volgens api.drupalcommerce zou MigrateDestinationEntityAPI::getKeySchema('koolborstel') moeten voldoen, maar dat werkt evenmin als bv. MigrateDestinationEntityAPI::getKeySchema('webform')

Voorlopige conclusie (april 2016):

  • Hier is geen snelle oplossing voor: Je moet dus werken met een producttype dat product heet. Na de migratie kan dat producttype wellicht hernoemd worden. Ik heb hier geen manier omheen gevonden, en het staat me bij uit de documentatie van CMU dat het per se op deze manier moet
  • De handleiding zegt dat je schema's handmatig kunt specificeren, maar ik weet niet of ik daar zo gelukkig van zou worden.

Worden afbeeldingen meegenomen?

Deze method incorporeert ook eventuele productafbeeldingen. Zie afbeelding.

Mapping productreferenties

In de CMU-taak CommerceMigrateUbercartNodeProduct heb ik doelveld

field_product_referentie	Product-referentie (commerce_product_reference)

maar wat voor waarde geef ik dat veld? En om het complexer te maken: De oorspronkelijke Übercart-producten zijn in aantal toegenomen als Drupal Commerce producttypes, ivm. dubbele SKU's (een mooi voorbeeld waar je in Drupal 7 displays voor zou gebruiken), dus misschien moet dat meegenomen worden in de mapping.

  • Tabel commerce_product bevat producten oftewel producttypes
  • Tabel node bevat nodes oftewel displays oftewel productnodes
  • Je kunt productnodes zonder productreferentie instantiëren. Alsof producttypes helemaal genegeerd kunnen worden - Nogal verwarrend
  • Als een display verwijst naar een productnode, maar je vult ook de display-velden in, dan verschijnen die velden allebei, bv. twee body-velden. De producttype-velden zijn daarbij duidelijk gelinkt en niet gekopiëerd

Voorbeeld

Dit betreft een database met een paar honderd producttypes en één productnode

Het gaat om producttype 0100. Hier staat-ie tussen de andere producttypes
Producttypes vind je in tabel commerce_product. Productype 0100 heeft product_id=1
Opnieuw producttype 0100 = product_id 1, nu via de web-UI
Tabel node bevat één record van het type product_display met nid/vid met waarde 201
Tabel field_product_referentie bevat de missende link. Dit veld maakt onderdeel uit van de node van de bundle product_display met nid/vid met waarde 201. Het refereert naar het producttype met ID 1

MigrateDestinationFile - Productafbeeldingen migreren

Migratie van afbeeldingen, bv. Übercart productafbeeldingen naar Drupal Commerce productafbeeldingen, vereist een eigen aanpak met behulp van DestinationClass MigrateDestinationFile. De Migrate-module biedt drie klasses:

  • MigrateFileUri: Standaard-klasse, oa. voor afbeeldingen
  • MigrateFileBlob: Voor oa. blob's
  • MigraterFileFid: Hobbyen met losse fid's

Van deze drie klasses, is MigrateFileUri de meest relevante. De uitleg hieronder heeft betrekking op deze klasse.

Primaire veldwaarde

  • De primaire waarde van deze klasse is een Uri of een locale bestandsspecificatie
  • De klasse gaat er vanuit, dat het doel een bestand is in de Drupal-bestandsstructuur

Subargumenten

  • source_dir: Prefix voor alle bestanden
  • destination_dir: Drupal-gedefineerde stream-wrapper, bv. public://. Standaardwaarde: public://
  • destination_file: Standaardwaarde: Bestandsnaam bronlocatie
  • file_replace: Wat te doen als het doelbestand al bestaat?
  • preserve_files: Wat te doen bij rollback?
  • urlencode: Wat te doen me speciale tekens indien doellocatie een URL betreft?

Voorbeeld migrate_d2d

De migrate_d2d-module migreert oa. bestanden. Dat geschiedt middels de code hieronder. Behalve toevoeging van commentaar, is deze code niet aangepast. Merk op dat in dit geval bestanden worden gemigreerd, los van eventuele nodes waar die bestanden bijhoren. Dat maakt het voor mij voor migratie van Übercart naar Drupal Commerce, van beperkt nut:

abstract class DrupalFileMigration extends DrupalMigration {
  public function __construct(array $arguments) {
    parent::__construct($arguments);
    
    // Uitlezen waardes webinterface
    //////////////////////////////////////////////////////
    //
    if (!empty($arguments['user_migration'])) {
       $user_migration = $arguments['user_migration'];
       $this->dependencies[] = $user_migration;
    }
    if (empty($arguments['bundle'])) {
      $arguments['bundle'] = 'file';
    }
    if (empty($arguments['file_class'])) {
      $arguments['file_class'] = 'MigrateFileUri';
    }
    if (empty($arguments['destination_dir'])) {
      $arguments['destination_dir'] = 'public://';
    }
    
    // SourceFields worden uitgebreid nav. input webinterface
    /////////////////////////////////////////////////////////
    //
    $this->sourceFields += $this->version->getSourceFields('file',$arguments['bundle']);
                                                           
    /////////////////////////////////////////////////////////                                                       
    // 1. Map classes
    /////////////////////////////////////////////////////////
    //
    // * Allow derived classes to override this definition by setting it before
    //   calling their parent constructor
    // * Voorbeeld van een (eventueel) handmatig gespecificeerde map
    //
    if (!isset($this->map)) 
    {
      $this->map = new MigrateSQLMap($this->machineName,
        array(
          'fid' => array(
            'type' => 'int',
            'unsigned' => TRUE,
            'not null' => TRUE,
            'description' => 'Source file ID',
            'alias' => 'f',
          ),
        ),
        MigrateDestinationFile::getKeySchema()
      );
    }
    
    /////////////////////////////////////////////////////////
    // 2. Bron
    /////////////////////////////////////////////////////////
    //
    // Bestanden zijn opgeslagen in een bestandssysteem, 
    // niet in een database. Daarom zijn oa. sourceFields
    // leeg (denk ik)
    //
    $this->source = new MigrateSourceSQL($this->query(),
    $this->sourceFields, NULL, $this->sourceOptions);

    /////////////////////////////////////////////////////////
    // 3. Doel
    /////////////////////////////////////////////////////////
    //
    // Gebruik MigrateDestinationFile als doel-klasse
    //
    $this->destination = new MigrateDestinationFile($arguments['bundle'],$arguments['file_class']);

    /////////////////////////////////////////////////////////
    // 4. Field mappings
    /////////////////////////////////////////////////////////
    //
    // * Dit zijn een handjevol van de subargumenten van
    //   MigrateDestinationFile
    //
    $this->addFieldMapping('destination_dir')
         ->defaultValue($arguments['destination_dir']);
    $this->addFieldMapping('source_dir')
         ->defaultValue($arguments['source_dir']);
    $this->addFieldMapping('file_replace')
         ->defaultValue(MigrateFile::FILE_EXISTS_REUSE);
    $this->addFieldmapping('preserve_files')
          ->defaultValue(TRUE);

    // UID
    /////////////////////////////////////////////////////////
    //
    if (isset($arguments['default_uid'])) {
      $default_uid = $arguments['default_uid'];
    }
    else {
      $default_uid = 1;
    }
    if (isset($user_migration)) {
      $this->addFieldMapping('uid', 'uid')
           ->sourceMigration($user_migration)
           ->defaultValue($default_uid);
    }
    else {
      $this->addFieldMapping('uid')
           ->defaultValue($default_uid);
    }
    
    // DNM?
    /////////////////////////////////////////////////////////
    //
    $this->addUnmigratedSources(array('filename', 'filemime', 'filesize', 'urlencode'));
  }
}

Zie ook

Bronnen

Algemeen

Migrate Community Documentation

MigrateDestinationEntityAPI::getKeySchema

Afbeeldingen migreren