DoctrineでRedshiftとMySQLの両方を扱うアプリケーションについて


[Symfony Advent Calender](http://qiita.com/advent-calendar/2014/symfony) 2014 22日目の記事です。


今日はRedshiftとMySQLを利用したアプリケーションを作っている経験から、いくつかお話させていただきたいと思います。
大した話はできませんが、皆様の参考になればいいと思います。


## Doctrineで普通にRedshift使うことができるか？
結局みなさんが気になるのはここだとは思うんですが、ほぼ問題なく使えます。
PostgreSQLベースなので、pdo_pgsqlを利用できるのでこれといって拡張しなきゃいけない部分はありません。

config.ymlの設定に関しても複数のデータベースに接続するときと同じような感じに設定すれば問題ありません。

``` config.yml
doctrine:
    dbal:
        default_connection: default
        connections:
            default:
                driver:   "%database_driver%"
                host:     "%database_host%"
                port:     "%database_port%"
                dbname:   "%database_name%"
                user:     "%database_user%"
                password: "%database_password%"
                charset:  UTF8
            redshift:
                driver:   "%redshift_database_driver%"
                host:     "%redshift_database_host%"
                port:     "%redshift_database_port%"
                dbname:   "%redshift_database_name%"
                user:     "%redshift_database_user%"
                password: "%redshift_database_password%"
                charset:  UTF8

    orm:
        auto_generate_proxy_classes: "%kernel.debug%"
        default_entity_manager: default
        entity_managers:
            default:
                connection: default
                mappings:
                    AppMysqlBundle: ~
            redshift:
                connection: redshift
                mappings:
                    AppRedshiftBundle: ~
```


## バンドルについて
サービスクラスやコントローラやビューファイルを置くバンドルとは別にMySQL,Redshiftそれぞれのエンティティやリポジトリを扱うバンドルを用意しています。
深い理由はありませんが、同じディレクトリにそれぞれのDB用のエンティティが入っていると開発時に混乱してしまうので、分けるようにしています。

```
├── src
│   ├── AppBundle # サービスとかコントローラとかがあるBundle
│   └── AppMysqlBundle # MySQL用のエンティティやリポジトリがあるBundle
│   └── AppRedshiftBundle # 
```


## マイグレーションの管理に関して
専用のマイグレーションバンドルを入れるわけではなくて、[DoctrineMigrationsBundle](https://github.com/doctrine/DoctrineMigrationsBundle)を利用しています。
最近まで私も知らなかったんですが、DoctrineMigrationsBundleには`--em`オプションがあり、どのエンティティマネージャを使うか選択できます。

今回のような設定とBundle構成になっている場合は`--em`で`redshift`か`default`を選択することにより、それぞれのマイグレーションファイルが生成されるようになっています。

``` mysql用のマイグレーションファイルの生成
$ app/console doctrine:migrations:diff
Generated new migration class to "/var/www/refract/current/app/DoctrineMigrations/Version20141221211144.php" from schema differences.
```

``` Redshift用のマイグレーションファイルの生成

$ app/console doctrine:migrations:diff --em=redshift
Generated new migration class to "/var/www/refract/current/app/DoctrineMigrations/Version20141221220404.php" from schema differences.
```

しかしこれだけだとマイグレーションを実行しようとした際にエラーになってしまいます。
それは`$this->abortIf()`のメソッドの影響で、マイグレーションを実行した際にエラーになってしまいます。

そこで生成されたマイグレーションを`$this>abortIf()`から`$this->skipIf()`に変更します。

```diff
# vim app/DoctrineMigrations/Version20141221211144.php (MySQL用)

<?php

namespace Application\Migrations;

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

/**
 * Auto-generated Migration: Please modify to your needs!
 */
class Version20141221211144 extends AbstractMigration
{
    public function up(Schema $schema)
    {
        // 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->skipIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
        $this->addSql('CREATE TABLE post (id INT AUTO_INCREMENT NOT NULL, title VARCHAR(255) NOT NULL, body LONGTEXT DEFAULT 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->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
++      $this->skipIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');

        $this->addSql('DROP TABLE post');
    }
}
```

```diff 
# vim app/DoctrineMigrations/Version20141221215556.php (Redshift用)
<?php

namespace Application\Migrations;

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

/**
 * Auto-generated Migration: Please modify to your needs!
 */
class Version20141221220404 extends AbstractMigration
{
    public function up(Schema $schema)
    {
        // this up() migration is auto-generated, please modify it to your needs
--      $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
++      $this->skipIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');

        $this->addSql('CREATE TABLE access_log (id SERIAL NOT NULL, user_agent VARCHAR(255) DEFAULT NULL, date TIMESTAMP WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
    }

    public function down(Schema $schema)
    {
        // this down() migration is auto-generated, please modify it to your needs
--      $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
++      $this->skipIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');

        $this->addSql('DROP TABLE access_log');
    }
}

```


基本的にはこのやり方で大丈夫なんですが、同じデータベースで違うサーバが複数台ある場合なんかもあるので、以前書いた「[DoctrineMigrationsBundleで複数データーベース対応できるようにしてみた](http://www.polidog.jp/2014/11/06/doctrine_migration/)」も参考にしてもらえると嬉しいです。

## serial(sequence)について
pdo_pgsqlを使っているため、マイグレーションで生成されるSQLもPostgreSQLのものになってしまいます。PostgreSQLと違い`serial`や`sequence`を使うことができません、代わりに`IDENTITY`を使用することができます。

ということで若干先ほどのマイグレーションファイルを修正する必要があります。

```diff 
# vim app/DoctrineMigrations/Version20141221215556.php (Redshift用)
...
-- $this->addSql('CREATE TABLE access_log (id SERIAL NOT NULL, user_agent VARCHAR(255) DEFAULT NULL, date TIMESTAMP WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
++ $this->addSql('CREATE TABLE access_log (id INT IDENTITY(1,1), user_agent VARCHAR(255) DEFAULT NULL, date TIMESTAMP WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');

```


## データを入れる場合のお話
ちょっとDoctrineやSymfony2の話から逸れてしまうかもしれませんが、Redshiftにデータを入れる場合ですが、１件程度ならDoctrineの`flash()`を使ってデータを入れてしまってもいいかもしれません。
ただ、Redshiftのドキュメントを見ていると大量のデータに関しては[copyコマンド](http://docs.aws.amazon.com/ja_jp/redshift/latest/dg/r_COPY.html)を推奨しています。

私の場合はFluent経由(fluent-plugin-redshift)で、Redshiftにデータを入れるようにしています。文字コードの問題とかもあるのですが、その辺はまた別の機会に説明したいと思います。

## 最後に
さっくりと書きましたが、基本的にほとんどトラブルもなくDoctrineからRedshiftが操作できるので、安心してDoctrineを使ってください。