この記事は、Drupal アドベントカレンダー 2020 の 7 日目です。
(後から参加したので記事の日付は遅れています…すみません)
みなさん、Migrate 使っていますか?
Migrate モジュールは Drupal 8 からコアに取り込まれた標準データ移行 API です。この記事では、Migrate を初めて使う人にはちょっとわかりづらい(かもしれない)エンティティ参照を扱う例として、画像ファイルをフィールドに取り込むサンプルを示します。
Migrate の基本的な使い方には触れませんが、コミュニティ有志による電子書籍『Drupal 9 Web 開発をはじめるための薄い本』(Drupal Meetup 豊田支部=編)で執筆を担当した章に詳しい手順を書かせていただきました。
こちら "薄い本" と銘打ちつつ、最新 Drupal 9 に関する内容満載!ボリューム満点の一冊です。是非ぜひチェックしてみてください。
やりたいこと
まず、やりたいことをまとめます。環境は、標準(standard)プロファイルでインストールした Drupal 9.1 サイトを前提とします。
- 記事(article)ノードに外部データをインポートする。
- 対象フィールドは以下とする:
- タイトル(title)
- 本文(body)
- 画像(field_image)
Drupal では、画像をファイル エンティティとして管理します。記事ノードの画像フィールドはファイル エンティティへの参照で、内部的には参照先エンティティの ID 値を保持します。このため、画像フィールドにデータを取り込むには、次の2つのステップが必要になります:
- 画像ファイルをファイルエンティティとしてインポートする。
- 記事ノードをインポートし、画像フィールドには 1. のファイルエンティティの ID 値をセットする。
画像ファイルのマイグレーション
次の3ファイルをインポートするとします:
このファイルを Drupal サイトのパブリック領域(public://)直下のファイルとしてインポートするマイグレーションの例を示します。簡単のため、ソースデータは embedded_data ソースプラグインを使用して YAML 内に埋め込んでいます。
id: mgdemo_image
source:
constants:
FILE_DIRECTORY: 'public://'
plugin: embedded_data
data_rows:
-
myid: 301
myimg: /tmp/img/cat.jpg
-
myid: 302
myimg: /tmp/img/gate.jpg
-
myid: 303
myimg: /tmp/img/snow.jpg
ids:
myid:
type: integer
process:
tmp_srcpath: myimg
tmp_filename:
plugin: callback
callable: basename
source: myimg
tmp_destpath:
plugin: concat
source:
- constants/FILE_DIRECTORY
- '@tmp_filename'
uri:
plugin: file_copy
source:
- '@tmp_srcpath'
- '@tmp_destpath'
file_exists: replace
move: false
destination:
plugin: 'entity:file'
記述内容を簡単に説明します。
メタ情報
id キーは、このマイグレーションの識別名。実行する時や他のマイグレーションから実行結果を参照する時などに使用する名前。
source 配下(データソースの定義)
サブキー constant は定数値を定義する。ここでは、FILE_DIRECTORY という名前でパブリック領域のストリーム名(public://)を指定している。これは後続の process 配下の記述で使用する。
各ファイルのフルパス(myimg)とそれぞれを一意に識別する番号(myid)をセットにして 1 つのレコードとし、これを 3 件分埋め込んである。
ids はレコードを一意に識別する列(データベース テーブルの主キーに相当)を指定する。この例では、myid を整数型の一意識別子として指定している。
process 配下(変換処理の定義)
疑似フィールドを使用している。これは、実際には存在しない名前のフィールドを処理中の一時変数として利用できるもので、名前の先頭に @ を付けることで値を参照できる。たとえば、疑似フィールド tmp_srcpath にはソースの myimg 列の値がセットされ、後続の処理で '@tmp_scrpath' と書いてその値を参照している。
サンプルでは次の 3 つの疑似フィールドを使用している:
- tmp_srcpath
コピー元のファイルのフルパス(myimg の値そのまま) - tmp_filename
パスを除去したファイル名 - tmp_destpath
コピー先のディレクトリパス
tmp_filename では、callback プラグインで PHP の basename 関数を呼び出して、フルパスのファイル名からディレクトリパスを除いたベース名を設定している。tmp_destpath では、concat プラグインを使用して、先述の FILE_DIRECTORY 定数と tmp_filename の値を連結して、コピー先ファイルのパスを生成している。
uri はファイルエンティティの URI を表すフィールドで、file_copy プラグインでファイルをコピーした結果を設定している。コピー元としてファイルのフルパス、コピー先として tmp_destpath 疑似フィールドの値をそれぞれ渡している。file_exists は同名ファイルが存在した場合の挙動を指定するオプションで、値 replace は置換を意味する。move はコピーか移動かを指定するオプションで、値 false は移動ではないこと、つまりコピー元のファイルは残すよう指示している。
destination 配下(データの格納先の定義)
entity:file プラグインを使用して、結果をファイルエンティティとして格納する。
以上です。
このマイグレーションを実行すると、/tmp/img ディレクトリの 3 ファイルがサイト内のパブリック ファイルとしてコピーされ、3 つのファイル エンティティが生成されます。
各ファイルの名前のリンクをクリックしてみると、パブリック領域 public://(既定では sites/default/files/)の下に、画像ファイルが JPEG 画像エンティティとしてインポートできていることがわかります。ここで注目したいのは、状態が「一時的」、利用場所が「0 places」になっている点。これは、各画像エンティティがまだどこからも参照されていないことを示しています。
Migrate モジュールによるエンティティの追跡
Migrate ではインポートしたエンティティを内部的に追跡しています。名前に migrate が含まれるテーブルを確認してみます。
$ drush sqlc
MariaDB [std]> show tables like 'migrate%';
+------------------------------+
| Tables_in_std (migrate%) |
+------------------------------+
| migrate_map_mgdemo_image |
| migrate_message_mgdemo_image |
+------------------------------+
2 rows in set (0.001 sec)
MariaDB [std]> desc migrate_map_mgdemo_image;
+-------------------+---------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------------+---------------------+------+-----+---------+-------+
| source_ids_hash | varchar(64) | NO | PRI | NULL | |
| sourceid1 | int(11) | NO | MUL | NULL | |
| destid1 | int(10) unsigned | YES | | NULL | |
| source_row_status | tinyint(3) unsigned | NO | | 0 | |
| rollback_action | tinyint(3) unsigned | NO | | 0 | |
| last_imported | int(10) unsigned | NO | | 0 | |
| hash | varchar(64) | YES | | NULL | |
+-------------------+---------------------+------+-----+---------+-------+
7 rows in set (0.001 sec)
migrate_map_mgdemo_image という名前のテーブルが、このマイグレーションでインポートされたエンティティを追跡するテーブルです。sourceid1 列がデータソース側の ID 値、destid1 列がインポート先エンティティの ID 値で、このテーブルによってデータソースのレコードと Drupal 側のエンティティとの対応関係が記録されています。
実際の値を確認すると次のようになります。
MariaDB [std]> select sourceid1,destid1 from migrate_map_mgdemo_image;
+-----------+---------+
| sourceid1 | destid1 |
+-----------+---------+
| 303 | 306 |
| 302 | 305 |
| 301 | 304 |
+-----------+---------+
3 rows in set (0.000 sec)
このマイグレーションでは、ファイルエンティティの ID に値を設定していないので、エンティティ作成時に自動採番され(304、305、306)、それぞれ対応するデータソース ID 値(myid 列の値)のレコードに記録されています。この対照表により、データソースの ID 値に対応するインポート済み Drupal エンティティを、後でいつでも調べることができます。
対照表のテーブルは「migrate_map_<マイグレーションID>」という名前になります。マイグレーション ID がわかれば、照会すべきテーブル名を特定できます。
記事のマイグレーション
画像ファイルのエンティティを確認したので、今度はそのエンティティを画像フィールドから参照するノードをインポートしてみましょう。マイグレーションの例を示します。
id: mgdemo_article
source:
plugin: embedded_data
data_rows:
-
myid: 1
mytitle: 吾輩は猫である
mytext: 吾輩は猫である。名前はまだない。
myimg: 301
-
myid: 2
mytitle: 羅生門
mytext: ある日の暮方の事である。一人の下人が、羅生門の下で雨やみを待っていた。
myimg: 302
-
myid: 3
mytitle: 雪国
mytext: 好きよあなた。いまでも。いまでも。
myimg: 303
ids:
myid:
type: integer
process:
nid: myid
title: mytitle
uid:
plugin: default_value
default_value: 1
body: mytext
field_image:
plugin: migration_lookup
migration: mgdemo_image
source: myimg
destination:
plugin: 'entity:node'
default_bundle: article
データソースの myimg 列の値は、先にインポートした画像のソース ID です。
タイトル(mytitle 列)と画像ファイルとの対応関係は次のとおり:
- 吾輩は猫である→ 301(/tmp/img/cat.jpg)
- 羅生門 → 302(/tmp/img/gate.jpg)
- 雪国 → 303(/tmp/img/snow.jpg)
マイグレーションの処理においては、画像ファイルのソース ID(301、302、303)から、インポート済み画像エンティティの ID 値(304、305、306)を照会して、画像フィールド(field_image)にセットする必要があります。
この仕事を担うのが migration_lookup プラグインです。
field_image: 配下の定義を抜粋すると、
field_image:
plugin: migration_lookup
migration: mgdemo_image
source: myimg
migration キーの値として、照会先の既存マイグレーションの名前(mgdemo_image)を指定しています。Migrate API は、この名前から照会先のテーブル(migrate_map_mgdemo_image)を特定し、source キーに指定したソース列(myimg)の値(301、302、303)から、それぞれに対応するインポート済み画像エンティティのID(304、305、306)を取得して、その値が field_image フィールドに設定されることになります。
画像、添付ファイル、タクソノミー、コメントなど、エンティティ参照として実装されるフィールドへのマイグレーションでは、この migration_lookup を用いたエンティティ間の紐づけが重要な役割を果たします。
上記マイグレーションを実行すると 3 件の記事ノードが作成され、各ノードの画像フィールドに、対応する画像エンティティが設定されます。
また、参照元の記事ノードがインポートされたことで、各ファイルエンティティの状態が「恒久的」、利用場所が「1 place」にそれぞれ変わります。
画像ファイルのパスを ID にする
上のサンプルでは、各画像ファイルの一意識別子として番号(301、302、303)を割り当てていました。しかし、よく考えると、画像ファイルのフルパスそれ自体が一意の識別名として使用できます。わざわざ番号を振らなくても、画像ファイルのパスを ID にして簡潔な形で書き換えることができますね。
画像ファイルのマイグレーション(改訂版)
id: mgdemo_image
source:
constants:
FILE_DIRECTORY: 'public://'
plugin: embedded_data
data_rows:
-
myimg: /tmp/img/cat.jpg
-
myimg: /tmp/img/gate.jpg
-
myimg: /tmp/img/snow.jpg
ids:
myimg:
type: string
process:
tmp_srcpath: myimg
tmp_filename:
plugin: callback
callable: basename
source: myimg
tmp_destpath:
plugin: concat
source:
- constants/FILE_DIRECTORY
- '@tmp_filename'
uri:
plugin: file_copy
source:
- '@tmp_srcpath'
- '@tmp_destpath'
file_exists: replace
move: false
destination:
plugin: 'entity:file'
ソースデータの myid 列を削除し、ids で myimg を文字列(string)型の ID 列として指定しています。
スキーマが変更されるので、migrate_map_mgdemo_image と migrate_message_mgdemo_image の各テーブルをいったん削除してから、新しいマイグレーションでインポートを実行すると、テーブルは次のようになりました。
MariaDB [std]> desc migrate_map_mgdemo_image;
+-------------------+---------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------------+---------------------+------+-----+---------+-------+
| source_ids_hash | varchar(64) | NO | PRI | NULL | |
| sourceid1 | varchar(255) | NO | MUL | NULL | |
| destid1 | int(10) unsigned | YES | | NULL | |
| source_row_status | tinyint(3) unsigned | NO | | 0 | |
| rollback_action | tinyint(3) unsigned | NO | | 0 | |
| last_imported | int(10) unsigned | NO | | 0 | |
| hash | varchar(64) | YES | | NULL | |
+-------------------+---------------------+------+-----+---------+-------+
7 rows in set (0.002 sec)
MariaDB [std]> select sourceid1,destid1 from migrate_map_mgdemo_image;
+-------------------+---------+
| sourceid1 | destid1 |
+-------------------+---------+
| /tmp/img/cat.jpg | 307 |
| /tmp/img/gate.jpg | 308 |
| /tmp/img/snow.jpg | 309 |
+-------------------+---------+
3 rows in set (0.001 sec)
ソースの ID がファイルのフルパス文字列に変わっていることがわかります。
記事ノードのマイグレーション(改訂版)
これに合わせて、記事ノードのソースデータは次のように書き換えることができます。
id: mgdemo_article
source:
plugin: embedded_data
data_rows:
-
myid: 1
mytitle: 吾輩は猫である
mytext: 吾輩は猫である。名前はまだない。
myimg: /tmp/img/cat.jpg
-
myid: 2
mytitle: 羅生門
mytext: ある日の暮方の事である。一人の下人が、羅生門の下で雨やみを待っていた。
myimg: /tmp/img/gate.jpg
-
myid: 3
mytitle: 雪国
mytext: 好きよあなた。いまでも。いまでも。
myimg: /tmp/img/snow.jpg
・・・以下変更なし
myimg 列の値を元の番号からファイルのパスに変更しています。migraton_lookup で照会するときのキーが番号から文字列に変わったことになりますが、対応関係に変更はないので、実行して得られる結果は同じです。
このように、画像ファイルのマイグレーションでは、ファイルパスを ID として使った方が簡単で直感的にわかりやすいかもしれません。
まとめ
画像フィールドを含むノードに、Migrate API を使用して文字列や画像ファイルをインポートする例を示しました。Migrate API はインポートしたエンティティを内部的に追跡しており、それを利用して 、migration_lookup プラグインでエンティティ間の参照関係を解決することができます。この仕組みは、タクソノミーやコメントなど、エンティティ参照に基づく他のフィールドでも同様に利用できます。
参考資料
- Migrate API overview(drupal.org)
- Migrate API(drupal.org)
- 31 days of Drupal migrations(understanddrupal.com)