DoctrineMigrationsBundleで複数データーベース対応できるようにしてみた

DoctrineMigrationsBundleで複数データーベース対応できるようにしてみた

November 6, 2014,
tags: php symfony doctrine2 migration


このエントリーをはてなブックマークに追加

Symfony2を使ってなにかサービスを使っていると時として複数のデータベースサーバが必要にある場合がありますよね。
もちろんSymfony2でもDBALのコネクションの設定とORMのエンティティマネージャーをの設定をすれば簡単に対応することができます。

そう、こんな感じにね。

$ vim app/config/config.yml


...
doctrine:
    dbal:
        default_connection: mysql
        connections:
            mysql:
                driver:   "%my_database_driver%"
                host:     "%my_database_host%"
                port:     "%my_database_port%"
                dbname:   "%my_database_name%"
                user:     "%my_database_user%"
                password: "%my_database_password%"
                charset:  UTF8
            postgres:
                driver:   "%pg_database_driver%"
                host:     "%pg_database_host%"
                port:     "%pg_database_port%"
                dbname:   "%pg_database_name%"
                user:     "%pg_database_user%"
                password: "%pg_database_password%"
                charset:  UTF8


    orm:
        auto_generate_proxy_classes: "%kernel.debug%"
        entity_managers:
            mysql:
                connection: mysql
                mappings:
                    PolidogAppMysqlBundle: ~
            postgres:
                connection: postgres
                mappings:
                    PolidogAppPostgresBundle: ~

マイグレーションが管理が出来ない??

ここまではいいんですが、DoctrineMigrationsBundleを利用してた場合にどうしましょう…。
2つの異なるリレーショナルデーターベース(MySQLとPostgreSQL)にまたがって管理しなければならない場合にたまたま遭遇して困ってしまいました。

解決探したところ「Issue #38 Multiple entity managers support 」というPRがあったのですが、どうやらマージされてないみたいです。。。

僕の場合の解決策

僕がどうやって解決したかというとマイグレーション用のクラスが継承している「AbstractMigration」いろいろ機能を持たせることができるのでそこを上手く活用して解決しました。

だいたいこんな感じの手順で対応していきます。

  1. バンドルの作成
  2. エンティティの作成
  3. マイグレーションファイルの生成
  4. 新たにAbstractMigrationを作成する
  5. MySQL用、PostgreSQL用のAbstractMigrationクラスを作成する
  6. マイグレーションファイルの継承先を変更する
  7. 実際にマイグレーションを実行する

1. バンドルの作成

実験環境用にバンドルをふたつ作ります。 今回は「PolidogAppMysqlBundle」と「PolidogAppPostgresBundle」の二つを用意します。

$ app/console generate:bundle --namespace=Polidog/App/MysqlBundle --format=yml
$ app/console generate:bundle --namespace=Polidog/App/PostgresBundle --format=yml

2.エンティティの作成

それぞれのバンドルにエンティティを作成していきます。 まずは、MySQL用のエンティティを作成します。

$ vim src/Polidog/App/MysqlBundle/Entity/Post.php

<?php
namespace Polidog\App\MysqlBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Class Post
 * @author polidogs@gmail.com
 *
 * @ORM\Entity()
 * @ORM\Table(name="posts")
 */
class Post
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="title", type="string", length=255, nullable=false)
     */
    private $title;


    /**
     * @var string
     *
     * @ORM\Column(name="content", type="text", nullable=false)
     */
    private $content;

}

次にPostgreSQL用のエンティティを作成します。

$ vim src/Polidog/App/PostgresBundle/Entity/Comment.php

/**
 * Class Comment
 * @author polidogs@gmail.com
 *
 * @ORM\Entity()
 * @ORM\Table(name="comment")
 */
class Comment
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var int
     * @ORM\Column(name="post_id", type="integer", nullable=false)
     */
    private $postId;

    /**
     * @var string
     * @ORM\Column(name="name", type="string", length=255, nullable=false)
     *
     */
    private $name;

    /**
     * @var string
     * @ORM\Column(name="comment", type="text", nullable=false)
     */
    private $comment;

}

エンティティの作成ができたら、generateしておきましょう。

$ app/console generate:doctrine:entities PolidogAppMysqlBundle --no-backup
$ app/console generate:doctrine:entities PolidogAppPostgresBundle --no-backup

3. マイグレーションファイルの生成

ここはいつもおなじみのマイグレーションファイルの生成です。

$ app/console doctrine:migrations:diff
$ app/console doctrine:migrations:diff --em=postgres

ここで一つ気をつけなきゃいけないのが、Postgres側で定義した、Comment.phpのエンティティのマイグレーションファイルを生成する場合は、–emにpostgresと指定してあげないとダメです。

4. 新たにAbstractMigrationを作成する

もともとの「Doctrine\DBAL\Migrations\AbstractMigration」クラスを継承した形のクラスを作成します。
さらに、ContainerAwareInterfaceもimplementsします。

$ vim src/Poildog/Util/AbstractMigration.php

<?php
namespace Polidog\App\Util;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Migrations\AbstractMigration as Original;
use Doctrine\DBAL\Schema\Schema;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

abstract class AbstractMigration extends Original implements ContainerAwareInterface
{
    /**
     * @var ContainerAwareInterface
     */
    protected $container;

    /**
     * @var string
     */
    protected $connectionName;

    public function preUp(Schema $schema)
    {
        $this->skipInvalidDB();
    }

    public function preDown(Schema $schema)
    {
        $this->skipInvalidDB();
    }

    /**
     * Sets the Container.
     *
     * @param ContainerInterface|null $container A ContainerInterface instance or null
     *
     * @api
     */
    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
    }

    /**
     * @throws \Doctrine\DBAL\Migrations\SkipMigrationException
     */
    protected function skipInvalidDB()
    {
        $dbName = $this->container->get('doctrine')->getConnection($this->$connectionName)->getDatabase();

        $connectionName = $this->createDatabaseId($this->connection);
        $targetName = $this->createDatabaseId($this->container->get('doctrine')->getConnection($this->$connectionName));

        $this->skipIf( $connectionName != $targetName, "Migration can only be executed on '{$dbName}' database (use --em={$this->$connectionName}).'" );
    }

    /**
     * @param Connection $connection
     * @return string
     */
    protected function createDatabaseId(Connection $connection)
    {
        $string = "";
        $params = $connection->getParams();
        foreach (array('host','port','dbname') as $key) {
            $string .= "_".$params[$key];
        }
        return $string;
    }
}

skipInvalidDBはマイグレーションが行われた際に、マイグレーションを実行するための正しい環境かどうかを判定させる役割です。

5. MySQL用、PostgreSQL用のAbstractMigrationクラスを作成する

次にAbstractMigrationを継承したクラスを作ります。
それぞれ「connectionName」パラメータに適切に名前を設定して行きます。
ここで設定する名前は、config.ymlのdoctrine.orm. entity_managersで指定してるそれぞれのキー名になります。
今回の場合だとmysqlとpostgresになります。

$ vim src/Polidog/App/Util/AbstractMysqlMigration.php

<?php

namespace Polidog\App\Util;


abstract class AbstractMysqlMigration extends AbstractMigration
{
    /**
     * @var string
     */
    protected $connectionName = "mysql";
}

MySQL用が作成できたら次はPostgreSQL用です。

$ vim src/Polidog/App/Util/AbstractPostgresMigration.php

<?php

namespace Polidog\App\Util;


abstract class AbstractPostgresMigration extends AbstractMigration
{
    /**
     * @var string
     */
    protected $connectionName = "postgres";
}

6. マイグレーションファイルの継承先を変更する

先ほど生成した2つのマイグレーションふぁいるの継承元を変更して行きます。
this->abortIfの判定を削除するのと、継承を変更します。

まずはMySQL側のPostテーブルのマイグレーションの変更しましょう。


$ vim app/DoctrineMigrations/Version20141105225419.php

<?php

namespace Application\Migrations;

use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
use Polidog\App\Util\AbstractMysqlMigration;

/**
 * Auto-generated Migration: Please modify to your needs!
 */
class Version20141105225419 extends AbstractMysqlMigration
{
    public function up(Schema $schema)
    {
        // this up() migration is auto-generated, please modify it to your needs
        $this->addSql('CREATE TABLE posts (id INT AUTO_INCREMENT NOT NULL, title VARCHAR(255) NOT NULL, content LONGTEXT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB');
    }

    public function down(Schema $schema)
    {
        // this down() migration is auto-generated, please modify it to your needs
        $this->addSql('DROP TABLE posts');
    }
}

次にPostgreSQL側を変更します。

$ vim app/DoctrineMigrations/Version20141105225451.php

<?php

namespace Application\Migrations;

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

/**
 * Auto-generated Migration: Please modify to your needs!
 */
class Version20141105225451 extends AbstractMigration
{
    public function up(Schema $schema)
    {
        $this->addSql('CREATE TABLE comment (id SERIAL NOT NULL, post_id INT NOT NULL, name VARCHAR(255) NOT NULL, comment TEXT NOT NULL, PRIMARY KEY(id))');
    }

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

7.実際にマイグレーションを実行する

最後にマイグレーションを実行して終わりになります。
まずは失敗すると怖いので--dry-runをつけて実行しましょう。

$ app/console doctrine:migrations:migrate --dry-run

Executing dry run of migration up to 20141105225451 from 0

  ++ migrating 20141105225419

     -> CREATE TABLE posts (id INT AUTO_INCREMENT NOT NULL, title VARCHAR(255) NOT NULL, content LONGTEXT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB

  ++ migrated (0.04s)

  SS skipped (Reason: Migration can only be executed on 'sftest' database (use --em=postgres).')

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

  ++ finished in 0.04
  ++ 2 migrations executed
  ++ 1 sql queries

これでpostsはcreate文が流れて、commentはsikipされますね。
次にcommentテーグルの方を確認してみましょう。

$ app/console doctrine:migrations:migrate --dry-run --em=postgres

Executing dry run of migration up to 20141105225451 from 0

  SS skipped (Reason: Migration can only be executed on 'sftest' database (use --em=mysql).')

  ++ migrating 20141105225451

     -> CREATE TABLE comment (id SERIAL NOT NULL, post_id INT NOT NULL, name VARCHAR(255) NOT NULL, comment TEXT NOT NULL, PRIMARY KEY(id))

  ++ migrated (0.02s)

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

  ++ finished in 0.02
  ++ 2 migrations executed
  ++ 1 sql queries

今度は、postsがスキップされて、commentテーブルのcreate文が流れました。
これで大丈夫だと思うので、実際にマイグレーションしましょう。

$ app/console doctrine:migrations:migrate
$ app/console doctrine:migrations:migrate --em=postgres

さて本当にできているか確認してみましょう。
まずはMySQLから。

$ mysql -u root -p
mysql>show tables;
+--------------------+
| Tables_in_sftest   |
+--------------------+
| migration_versions |
| posts              |
+--------------------+
2 rows in set (0.00 sec)

ちゃんとpostsテーブルが生成されていますね。 次は、PostgreSQLを確認します。

sftest=# \d
 public | comment            | table    | polidog
 public | comment_id_seq     | sequence | polidog
 public | migration_versions | table    | polidog

コメントのみテーブルが反映できていますね。 これで無事設定できました!

最後に

今日検証したサンプルのコードはGithubで公開しています。 - polidog/doctrine-migrations-multiple

まあわりと無理矢理ですが、複数のデータベースが存在する場合でもマイグレーションは可能なわけです。
ただし、migration_versionsには全てのマイグレーションの情報が記録されてしまうのでちょっとイケてないです。

MySQL側

mysql> SELECT * FROM migration_versions;
+----------------+
| version        |
+----------------+
| 20141105225419 |
| 20141105225451 |
+----------------+
2 rows in set (0.00 sec)

PostgreSQL側

sftest=# SELECT * FROM migration_versions;
 20141105225419
 20141105225451

本当ならば、データベースごとにマイグレーションのディレクトリが変わって全く別管理で行けると一番理想的なんですが・・・
まあDoctrineMIgrationsBundleが進化してくれる事を期待したいと思います。
あーあと、他にもっといい方法がある方はぜひ教えてください!!

おしまい。

参考

comments powered by Disqus