プログラミング - THIS IS IT !

より良い開発をすべく日々奮闘しているプログラマーのブログです。設計に興味があります。主にPHPネタを書いてます 日本Symfonyユーザー会

DoctrineMigrationsBundleの使い方について - Symfony Advent Calender 2012 9日目

こんにちは。@です。

この記事は Symfony Advent Calender 2012 Day9の記事です。
Symfony界のイケメンさんからバトンを引き継ぎ次に繋ぎたいと思います。
昨日の Day8の記事ではSymfonyユーザー会やそのコミュニティーについてご紹介されています。

この記事では、あまり日本語情報のないDoctrineMigrationsBundleについて紹介したいと思います。

マイグレーションとは

Railsでは言わずと知れた機能ですね。通常DBテーブルを新規に作成したり、カラムの追加、変更を行うにはSQLを実行するのですが、マイグレーション機能では事前にスクリプトファイル(=マイグレーションファイル)さえ用意しておけばコマンドを実行することでDBスキーマの変更を容易に行うことができます。
DBの変更の度にSQLファイルをコミットして、他の開発者にはそれを流してもらうのは非効率ですのでコマンド一発で実行できるこの機能は非常に便利です。

コマンドから容易にスキーマ変更を行えるといことは、デプロイの自動化にも利用できます。公式ドキュメントにも記載されている通り、

またマイグレーションの機能として、変更したスキーマを元に戻すこともできますので、万一リリースで問題が発生した場合にも簡単に切り戻しを行うこともできます。
また、Git等のバージョン管理を用いて開発している場合、そのコミット時点でのDBスキーマに戻すこともできます。

Symfony2には公式ドキュメントにも記載されている「DoctrineMigrationsBundle」という素晴らしいバンドルがあるのでこれを利用することで簡単にマイグレーション機能を利用することができます。

公式ドキュメント(英語)
Symfony2.1系
http://symfony.com/doc/current/bundles/DoctrineMigrationsBundle/index.html
Symfony2.0系
http://symfony.com/doc/2.0/bundles/DoctrineMigrationsBundle/index.html
Doctrine Migration
http://docs.doctrine-project.org/projects/doctrine-migrations/en/latest/toc.html

対象

対象バージョン

この記事ではSymfony2.0系での説明をしていきます。

対象データベースはMySQLになります。PostgreSQL等他のDBでの動作は検証していませんし、MySQL前提で話を進めます。

検証環境

Symfony 2.0.19
Mysql 5.5.27
php 5.3.17

インストール

Symfony2.0系のインストール方法です。Symfony2.1系をお使いの方は適宜読み替えて下さい。

depsファイルへの記述

[doctrine-migrations]
    git=http://github.com/doctrine/migrations.git

[DoctrineMigrationsBundle]
    git=http://github.com/doctrine/DoctrineMigrationsBundle.git
    target=/bundles/Symfony/Bundle/DoctrineMigrationsBundle
    version=origin/2.0

vendorライブラリの更新&インストール

$ php bin/vendors install

次に、bundleの登録を行います。
app/autoload.phpとapp/AppKernel.phpファイルに書き足します。

app/autoload.php

<?php
$loader->registerNamespaces(array(
    //...
    'Doctrine\\DBAL\\Migrations' => __DIR__.'/../vendor/doctrine-migrations/lib',
    'Doctrine\\DBAL'             => __DIR__.'/../vendor/doctrine-dbal/lib',
));

app/AppKernel.php

<?php
public function registerBundles()
{
    $bundles = array(
        //...
        new Symfony\Bundle\DoctrineMigrationsBundle\DoctrineMigrationsBundle(),
    );
}

コマンド一覧

php app/console とすると実行できるコメンドの一覧が表示されます。

doctrine:migrations:diff Generate a migration by comparing your current database to your mapping information.
doctrine:migrations:execute Execute a single migration version up or down manually.
doctrine:migrations:generate Generate a blank migration class.
doctrine:migrations:migrate Execute a migration to a specified version or the latest available version.
doctrine:migrations:status View the status of a set of migrations.
doctrine:migrations:version Manually add and delete migration versions from the version table.


それぞれのコマンドが取れる引数は--helpで確認して下さい。
例)

php app/console doctrine:migrations:diff --help

Usage:
 doctrine:migrations:diff [--editor-cmd[="..."]] [--configuration[="..."]] [--db-configuration[="..."]] [--filter-expression[="..."]] [--em[="..."]]

Options:
 --editor-cmd         Open file with this command upon creation.
 --configuration      The path to a migrations configuration file.
 --db-configuration   The path to a database connection configuration file.
 --filter-expression  Tables which are filtered by Regular Expression.
 --em                 The entity manager to use for this command.

Help:
 The doctrine:migrations:diff command generates a migration by comparing your current database to your mapping information:
 
     app/console doctrine:migrations:diff
 
 You can optionally specify a --editor-cmd option to open the generated file in your favorite editor:
 
     app/console doctrine:migrations:diff --editor-cmd=mate

アーキテクチャ

DoctorineMigrationBundleのアーキテクチャは非常にシンプルです。

使い方

それでは使い方を順を追って解説していきます

はじめ方

doctrine:migrations:status(初期化と状態の確認)

まずはこのコマンドを実行し、初期化します。

$ php app/console doctrine:migrations:status

 == Configuration

    >> Name:                                   Application Migrations
    >> Database Driver:                        pdo_mysql
    >> Database Name:                          my_symfony
    >> Configuration Source:                   manually configured
    >> Version Table Name:                     migration_versions
    >> Migrations Namespace:                   Application\Migrations
    >> Migrations Directory:                   /private/var/www/html/My_Symfony/app/DoctrineMigrations
    >> Current Version:                        0
    >> Latest Version:                         0
    >> Executed Migrations:                    0
    >> Executed Unavailable Migrations:        0
    >> Available Migrations:                   0
    >> New Migrations:                         0
Current Version
今のバージョン
Latest Version
最新のバージョン
Executed Migrations
実行したマイグレーション
Executed Unavailable
Migrations実行したけど利用できなかったマイグレーション数(SQLExceptionが発生した等何かしらの要因で実行できなかったファイル数と思われる。未確認)
Available Migrations
利用可能なマイグレーション(「app/DoctrineMigrations/」以下にあるマイグレーションファイル)

:New Migrations 新しく実行するマイグレーション

この時、データベースに「migration_versions」というテーブルが作成されます。

マイグレーションファイルの作成

doctrine:migrations:generate(マイグレーションファイルの雛形生成)

このコマンドでマイグレーションファイルの雛形を生成することができます。DBスキーマを変更する場合にはこのコマンドを実行します。

デフォルトでは「app/DoctrineMigrations/」ディレクトリ以下に生成され、ファイル名は「Version【現在日時】.php」となます。

<?php
// app/DoctrineMigrations/Version20121209120000.php

namespace Application\Migrations;

use Doctrine\DBAL\Migrations\AbstractMigration,
    Doctrine\DBAL\Schema\Schema;

class Version20121209120000 extends AbstractMigration
{
    // DB変更する内容
    public function up(Schema $schema)
    {
    }
    // 元に戻す内容
    public function down(Schema $schema)
    {
    }
}

マイグレーションファイルに記述をします。マイグレーションファイルには、今回変更したい内容と、その変更を戻したい場合のメソッドの2つを定義します。

記述の仕方は2パターンあります。1つ目はDoctrineのSchemaクラスを用いて記述する方法で2つ目がSQLを直接記述する方法です。以下のサンプルはUserテーブルを新しく追加する場合のコードになります。

<?php
    :
    public function up(Schema $schema)
    {
        $table = $schema->createTable('user');
        $table->addColumn('id', 'integer', array('autoincrement' => true, 'comment' => 'PK'));
        $table->addColumn('username', 'string', array('length' => '255', 'notnull' => true,));
          :
        $table->addColumn('created_at', 'datetime', array('notnull' => true, 'comment' => '作成日時'));
        $table->addColumn('updated_at', 'datetime', array('notnull' => true, 'comment' => '更新日時'));
        $table->setPrimaryKey(array('id'));
    }
    public function down(Schema $schema)
    {
        $schema->dropTable('user_append')
    }
}

ただし、見ての通り非常に記述が面倒くさいので以下の通りSQLで書くことをオススメします。こちらのほうが直感的に書けると思いますし、DoctrineのMappingは完璧ではないのでそれも含めてこちらで記述しておいた方が良いでしょう。
例えばDoctrineではtimestamp型を扱えないのですが、どうしても扱いたい場合には下記の様に記述します。

<?php
    :
    public function up(Schema $schema)
    {
        $this->addSql('
            CREATE TABLE `user` (
              `id` int NOT NULL AUTO_INCREMENT,
              `username` varchar(255) NOT NULL,
              `password` varchar(255) NOT NULL,
              `salt` varchar(255) NOT NULL,
              `created_at` datetime` NOT NULL,
              `updated_at timestamp` NOT NULL,
              PRIMARY KEY (`id`)
            ) ENGINE=InnoDB
        ');
    }
    public function down(Schema $schema)
    {
        $this->addSql('DROP TABLE user');
    }
}

マイグレーションファイルを記述する時には変更を元に戻すDownメソッドは気を付けて記述するようにしましょう。ここを間違えているときちんと元のバージョンに戻すことができなくなります。

doctrine:migrations:migrate(特定バージョンor最新バージョンへマイグレーションの実行)

このコマンドでマイグレーションを実行できます。このコマンドをversionの引数なしで実行すると最新バージョンになるようマイグレーションが実行されます。

$ php app/console doctrine:migrations:migrate
                                                              
                    Application Migrations                    
                                                              
WARNING! You are about to execute a database migration that could result in schema changes and data lost. Are you sure you wish to continue? (y/n)y   ←yを入力しEnter
Migrating up to 20121209120000 from 0

  ++ migrating 20121209120000
     -> 
            CREATE TABLE `user` (
                `id` int NOT NULL AUTO_INCREMENT,
                `username` varchar(255) NOT NULL,
                `password` varchar(255) NOT NULL,
                `salt` varchar(255) NOT NULL,
                `created_at` datetime NOT NULL,
                `updated_at` timestamp NOT NULL,
                PRIMARY KEY (`id`)
            ) ENGINE=InnoDB

  ++ migrated (0.07s)

  ------------------------

  ++ finished in 0.07
  ++ 1 migrations executed
  ++ 1 sql queries

データベースを見るとバージョン20121209120000のマイグレーションが実行されていることが分かります。「doctrine:migrations:status」コマンドはversionカラムとマイグレーションファイルを比較することで状態を判定しているようです。

mysql> select * from migration_versions;
+----------------+
| version        |
+----------------+
| 20121209120000 |
+----------------+

カラムの追加(や変更)も同様にマイグレーションファイルを記述します。

<?php
    :
    public function up(Schema $schema)
    {
        $this->addSql('ALTER TABLE user ADD COLUMN lastname varchar(255) NULL AFTER salt');
        $this->addSql('ALTER TABLE user ADD COLUMN firstname varchar(255) NULL AFTER lastname');
    }
    public function down(Schema $schema)
    {
       $this->addSql('ALTER TABLE user DROP COLUMN lastname'); 
       $this->addSql('ALTER TABLE user DROP COLUMN firstname');
    }
}

実行

$ php app/console doctrine:migrations:migrate

マイグレーションを用いてバージョン管理

DBを戻したくなったり特定のバージョンに進めたい場合には、versionを指定して実行することで行えます。

例)
$ php app/console doctrine:migrations:migrate 20121209150000 --no-interaction

ちなみにですが、「--no-interaction」を付けることで毎回yes/noを聞かれることがなくなります。自動デプロイ時には必須なので知っておくと便利です。(先ほど紹介した通りコマンドの後に「--help」を付けるとオプションが確認できます)

doctrine:migrations: execute(特定のマイグレーションバージョンだけ実行)

あまり利用することはないと思いますが、特定のマイグレーションだけ実行したり戻したりすることもできます。

// バージョン20121209120000で実行した変更だけを戻す
$ php app/console doctrine:migrations:execute 20121209150000 --down
// バージョン20121209120000の変更だけ実行
$ php app/console doctrine:migrations:execute 20121209150000 --up
doctrine:migrations: version(migration_versionsテーブルから特定バージョンを追加or削除)

これもあまり使うことはないかと思います。が一応説明を。

「doctrine:migrations: execute」を使った場合など、データベースとマイグレーションファイルに不整合が起こる場合があります。
例)php app/console doctrine:migrations:execute 20121209120000 --down を実行してuser テーブルを削除したけど、「20121209150000」は「migration_versions」テーブルに残っている。「20121209150000」はuserテーブルのカラム変更なので実行しようとすると「SQLException」が発生する。

そういう場合はこのコマンドを用いて手動で削除することができます。

$ php app/console doctrine:migrations:version 20121209150000 --delete

マイグレーションファイルの自動生成

マイグレーションファイルの自動生成もできます。

doctrine:migrations:diff

このコマンドは現在のDBとマッピング情報を比較して、マイグレーションファイルを生成してくれます。マッピング情報を書いてこのコマンドを実行するとマイグレーションファイルを生成してくれます。

例えばgroupテーブルのマッピング情報(annotation、yml等)を新規に用意してコマンドを実行すると以下のようなマイグレーションファイルが生成されます。

<?php
    :
    public function up(Schema $schema)
    {
        $this->addSql("CREATE TABLE group (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY(id)) ENGINE = InnoDB");
    }

    public function down(Schema $schema)
    {
        $this->addSql("DROP TABLE group");
    }
}

Doctrineがマッピングできない情報を利用したり(先ほどのtimestampなど)カラム追加する場合には多少修正してやる必要があります。
マッピング情報に「birthday」を足して実行した場合以下のようなファイルが生成されます。

<?php

namespace Application\Migrations;

use Doctrine\DBAL\Migrations\AbstractMigration,
    Doctrine\DBAL\Schema\Schema;

class Version20121209190000 extends AbstractMigration
{
    public function up(Schema $schema)
    {
        $this->abortIf($this->connection->getDatabasePlatform()->getName() != "mysql");        
        $this->addSql("ALTER TABLE user ADD birthday DATE DEFAULT NULL, CHANGE updated_at updated_at DATETIME NOT NULL");
    }

    public function down(Schema $schema)
    {
        $this->abortIf($this->connection->getDatabasePlatform()->getName() != "mysql");
        $this->addSql("ALTER TABLE User DROP birthday, CHANGE updated_at updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL");
    }
}

1つ目「updated_at」のtimestamp型がdatetime型に変更されてしまう。
→CHANGE updated_at〜の部分を消す。(datetime型でもういい気がします)

2つ目は「birthday」カラムがUserテーブルの一番最後にカラム追加されてしまう。
→好みの問題ですが、カラムの順番を気にする人もいるでしょう。「ALTER TABLE user ADD birthday DATE DEFAULT NULL AFTER firstname」などとしておくと任意の場所にカラム追加できて良いと思います。

「doctrine:migrations:diff」を使いつつ、必要に応じて修正するのが良いのではないかと思います。

まとめ

  • DoctrineMigrationsBundleを使えば、データベースの更新作業を簡単に共有できます。
  • 特定のデータベーススキーマに戻すことも容易です。


主な使い方についてはほとんど解説したつもりですが、ちょっとしたtipsなど時間があればまた紹介したいと思います。

引き続き「Symfony Advent Calender 2012」をお楽しみ下さい。明日は、@77webさんの「Symfony2で翻訳ファイルを使ってフォームを日本語化する方法」の予定です。

以上、「DoctrineMigrationsBundleの使い方について 」でした。