[Symfony] Doctrine Migrationsでデータ更新のマイグレーションスクリプトを書く
Symfonyで DoctrineMigrationsBundle は導入してるけど、
$ bin/console doctrine:migrations:diff
$ bin/console doctrine:migrations:migrate
しか使ってない人向けの記事です。
スキーマの変更だけでなく、既存データの整形などをマイグレーションスクリプトで行いたいこともある
例えば json
DBAL Typeのカラムを array
DBAL Typeに変更するようなマイグレーションを行うとします。
bin/console doctrine:migrations:diff
すると、以下のようなマイグレーションスクリプトが作られます。
final class Version20200418144157 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE user CHANGE roles roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\'');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE user CHANGE roles roles JSON NOT NULL');
}
}
この例では、 user
テーブルの roles
カラムを変更しています。
カラムの型を JSON
から LONGTEXT
に変更して '(DC2Type:array)'
とDoctrine用のコメントを追加していますね。
このマイグレーションスクリプトを実行すれば、確かにスキーマは適切に変更できます。
が、もしすでに roles
カラムにjson型式のデータが入っていたら、スキーマ変更後にはアプリからデータが正しく読み出せなくなってしまいます。
なぜなら、DBAL Typeが json
の場合と array
の場合とで、Doctrineが期待しているDBの実データの形式が異なるからです。
json
の場合は、 ["ROLE_ADMIN"]
のようなjson型式の文字列がDBに入っていることを期待しているのに対し、 array
の場合は a:1:{i:0;s:10:"ROLE_ADMIN";}
のように serialize された文字列を期待しています。
スキーマだけを array
に変更して中身が json
のままだと、Doctrineは ["ROLE_ADMIN"]
という文字列を unserialize しようとして、当然エラーになります。
つまり、こういう場合はマイグレーションスクリプトで スキーマの変更だけでなくデータの整形も一緒に実施してあげる必要がある のです。
具体的なやり方
やり方はとても簡単です。今回の場合なら、マイグレーションスクリプトに postUp()
と preDown()
メソッドの実装を追加して、その中で既存のデータを更新するスクリプトを実行するようにすればよいです。
具体的には、以下のようなコードで実現できます。
final class Version20200418144157 extends AbstractMigration
{
// ...
public function postUp(Schema $schema): void
{
$paramSets = [];
$users = $this->connection->query('SELECT id, roles FROM user')->fetchAll();
foreach ($users as $user) {
$paramSets[] = [
'id' => $user['id'],
'roles' => serialize(json_decode($user['roles'])), // ここがポイント
];
}
foreach ($paramSets as $paramSet) {
$this->addSql('UPDATE user SET roles = :roles WHERE id = :id', $paramSet);
}
}
public function preDown(Schema $schema): void
{
$paramSets = [];
$users = $this->connection->query('SELECT id, roles FROM user')->fetchAll();
foreach ($users as $user) {
$paramSets[] = [
'id' => $user['id'],
'roles' => json_encode(unserialize($user['roles'])), // ここがポイント
];
}
foreach ($paramSets as $paramSet) {
$this->addSql('UPDATE user SET roles = :roles WHERE id = :id', $paramSet);
}
}
}
// ここがポイント
と書いた箇所がポイントです。
もともとDBに入っていたデータを、
-
postUp()
では 1.json_decode()
してから 2.serialize()
している -
preDown()
では 1.unserialize()
してから 2.json_encode()
している
ことが分かるでしょう。
つまり、
-
up
の際は- まずスキーマが
json
からarray
に変更される - その後、もともとjson型式だった実データをserializeされた型式に変換して更新する
- まずスキーマが
-
down
の際は- まずserializeされた型式の実データをjson形式に変換して更新する
- その後、スキーマが
array
からjson
に変更される
ということが起こります。
スキーマが json
の状態でserializeされた型式の文字列を書き込もうとするとDBレイヤーで「正しいjson型式じゃないですよ」というエラーになってしまうので、順番に要注意です。
ちなみに、今回はデータ整形の内容がスキーマ変更に関連したものだったので postUp()
と preDown()
の中で行いましたが、データ整形のマイグレーションスクリプトを別ファイル(スキーマ変更の直後のバージョン)にしてしまっても別に構いません。
おまけ:SQLの実行方法について
上記の例で、データ更新のためのSQLは以下のように $this->addSql()
メソッドを使って実行しました。
foreach ($paramSets as $paramSet) {
$this->addSql('UPDATE user SET roles = :roles WHERE id = :id', $paramSet);
}
ここも、 SELECT
の部分と同じように直接 $this->connection
を触って実行させることも出来るには出来ます。
$this->connection->beginTransaction();
try {
$updateStatement = $this->connection->prepare('UPDATE user SET roles = :roles WHERE id = :id');
foreach ($paramSets as $paramSet) {
$updateStatement->execute($paramSet);
}
$this->connection->commit();
} catch (\Throwable $e) {
$this->connection->rollBack();
throw $e;
}
が、これだと --dry-run
オプションをつけていても実際に UPDATE
クエリが走ってしまう ので、更新系のクエリは $this->addSql()
に登録して遅延実行させるようにするべきでしょう。
まとめ
- Doctrine Migrationのマイグレーションスクリプトは自由にSQLを書けるので、スキーマの変更だけでなくデータの整形をしたい場合は手動でスクリプトを書けばOK
-
preUp()
postUp()
preDown()
postDown()
といったメソッドが用意されているので、上手く使うときれいに書ける
Discussion