CakePHP3のMigrationsでDB接続先を切り替える方法

CakePHP3 の Migrations コマンドを使ってマイグレーションする際、DB 接続先を明示的に指定することが出来ます。

この記事の内容は、以下のように CakePHP の設定ファイルに複数の Datasource が設定されていることが前提になります。

config/app.php:

    /**
     * Connection information used by the ORM to connect
     * to your application's datastores.
     * Do not use periods in database name - it may lead to error.
     * See https://github.com/cakephp/cakephp/issues/6471 for details.
     * Drivers include Mysql Postgres Sqlite Sqlserver
     * See vendor\cakephp\cakephp\src\Database\Driver for complete list
     */
    'Datasources' => [
        'default' => [
            // 省略
        ],
        'my_other_connection' => [
            // 省略
        ]
    ],

Datasource を指定するオプション

例えば config/app.php で設定した my_other_connection の Datasource に接続する場合、以下のように -c オプションにデータソース名を指定します。

./bin/cake migrations migrate -c my_other_connection

また、以下のように接続先を指定しないとデフォルトの Datasource が使用されます。

./bin/cake migrations migrate

ただし、この方法は全てのマイグレーションファイルで指定した接続先が使用されます。マイグレーションファイルごとに接続先を切り替えたい場合は以下を参照してください。

マイグレーションファイルごとに接続先を切り替える方法

少しアナログな方法にはなりますが、少し手を加えることで実現できます。

まず、以下のように接続先ごとにフォルダを分けて、その中にマイグレーションファイルを入れておきます。

  • config/Migrations/default (default コネクション用フォルダ)
  • config/Migrations/other (my_other_connection コネクション用フォルダ)

この状態でマイグレーションをするときに -s (–source=SOURCE) オプションで上記のディレクトリを指定してマイグレーションします。

./bin/cake migrations migrate -c default -s Migrations/default

./bin/cake migrations migrate -c my_other_connection -s Migrations/other

マイグレーションの実行履歴はコネクションごとの phinxlog テーブル上で管理されるため、バッティングせずにマイグレーション出来ます。

コマンドオプション指定の組み合わせは間違えないよう注意する必要があります。

商品の販売スケジュールを管理するテーブル設計パターン

商品とその販売スケジュールを管理するためのテーブル設計をするとき、販売スケジュールの情報を別テーブルに分離しておくと再利用性の高い作りに出来る場合があります。一つの設計パターンとしてメモしておきます。

例えば、以下の様な販売方法に対応したいとします。

  • 同時に複数の商品を販売する
  • 期間ごとに販売する商品の組み合わせ(商品セット)が異なる
  • 一度販売した商品の組み合わせは以降も使いまわす場合がある

つまりこういったケースです。

  • 1月に販売する商品
    • 商品A
    • 商品B
  • 2月に販売する商品
    • 商品C
  • 3月に販売する商品(1月の商品セットの使い回し)
    • 商品A
    • 商品B

テーブル設計例

products テーブル(商品)

  • id INT AUTO_INCREMENT
  • name VARCHAR COMMENT ‘商品名’

product_sets テーブル(商品セット)

  • id INT AUTO_INCREMENT
  • name VARCHAR COMMENT ‘商品セット名’
  • is_default BOOL COMMENT ‘基本商品セットフラグ’

product_set_relations テーブル(商品セットに属する商品)

  • id INT AUTO_INCREMENT
  • product_set_id INT
  • product_id INT

product_set_sale_schedules テーブル(商品セットの販売スケジュール)

  • id INT AUTO_INCREMENT
  • product_set_id INT
  • start_datetime DATETIME COMMENT ‘販売開始日時’
  • end_datetime DATETIME COMMENT ‘販売終了日時’

上記スキーマの解説

まず products テーブルに商品情報を持ちます。商品の種類の数だけレコードが出来ます。

そして複数の商品をまとめた商品セットを product_sets としてまとめ、その商品セットの販売期間の情報を product_set_sale_schedules に持つという構成です。

product_set_relations テーブルは多:多のリレーションを解消するための中間テーブルです。商品セットに属する商品一覧の情報を持ちます。

一度販売した商品セットは今後のスケジューリングでも使い回すことが出来ます。

もし期間に該当する商品セットがあった場合、それが優先して有効な商品セットとなりますが、該当するスケジュール情報が無かった場合は is_default が立っている基本商品セットが有効になります。こうしておくことで該当する販売スケジュールのデータが無かった場合のフォールバックが可能となり、常に何かを販売し続けることができます。(その必要が無ければ不要)

また、過去の販売スケジュールの履歴はスケジュールテーブルに蓄積して残すことが出来るため、後から過去の販売スケジュールを確認することができます。

今回の設計パターンについて補足

今回は少し要件が特殊だと思うので、実際には要件に合わせて構成を見なおした方がいいと思います。(設計パターンとか書いておいてすみません。)

例えば今回用いた「商品セット」という概念は、商品の組み合わせの再利用をしやすくする一方で、細やかな組み合わせの変更を難しくしています。

再利用されるケースが少ない場合は商品セットテーブルを無くしてしまい、商品ごとにスケジュールを設定する構成にしたほうが柔軟に対応できるでしょう。

「運用でカバー」は悲しい

運用でカバーという名のもと無駄なデータが生まれていくのは設計者として悲しいことです。

要件をよく確認して適切にテーブル設計をしていきたいものです。

MySQLの複合主キーテーブル検索時の速度比較

MySQL の複合主キーテーブルのレコードを何件かピックアップして主キー検索したい機会があったので、いくつかのパターンの SQL で検索してパフォーマンス比較をしてみました。

実験の内容

  • それぞれ適当にピックアップした15レコードを1クエリで検索する
  • 実行時間は同じクエリを5回実行したときの最小と最大の時間を計測
  • レコードは適当に100,000件投入

実験環境

  • Ubuntu 13.10 (on Vagrant)
  • MySQL 5.5.37
  • InnoDB

使用したテーブル

CREATE TABLE `myTable` (
  `player_id` int(11) NOT NULL,
  `id` int(11) NOT NULL,
  `body` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`player_id`,`id`)
) ENGINE=InnoDB;

各検索方法の実行時間とインデックス使用状況

1. OR検索の場合

SQL例

SELECT * FROM `myTable`
WHERE  (`player_id` = 50 AND `id` = 4856)
  OR (`player_id` = 61 AND `id` = 4274);

※実験時は15個のORを繋げて検索しましたが長くなるのでここでは省略しています。

実行時間

1.2ms ~ 1.5ms

EXPLAIN結果

id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE myTable range PRIMARY PRIMARY 8 NULL 15 Using where

2. UNION ALL検索の場合

SQL例

SELECT * FROM `myTable` WHERE (`player_id` = 50 AND `id` = 4856)
UNION ALL
SELECT * FROM `myTable` WHERE (`player_id` = 61 AND `id` = 4274);

実行時間

1.5ms ~ 2.0ms

EXPLAIN結果

id select_type table type possible_keys key key_len ref rows Extra
1 PRIMARY myTable const PRIMARY PRIMARY 8 const,const 1
2 UNION myTable const PRIMARY PRIMARY 8 const,const 1
15 UNION myTable const PRIMARY PRIMARY 8 const,const 1
NULL UNION RESULT ALL NULL NULL NULL NULL NULL

3. WHERE IN の複合カラム指定による検索の場合

SQL例

SELECT * FROM `myTable`
WHERE (player_id, id) IN ((50, 4856),(61, 4274));

実行時間

40.9ms ~ 49.7ms

EXPLAIN結果

id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE myTable ALL NULL NULL NULL NULL 100714 Using where

コメント

最も高速なのは普通に OR で繋げる検索でした。range スキャンになるんですね。次点で UNION ALL 検索。ちなみに ALL 無しの UNION に変えてもほぼ同じ速度でした。

そして最も遅かったのは WHERE IN 検索でした。IN 句に指定する件数が多いとオプティマイザはフルスキャンを選択するため大幅に遅い結果になりました。