コネヒト開発者ブログ

コネヒト開発者ブログ

CakePHP2用のMaster/Replica接続管理プラグインをOSS化しました

こんにちは、サーバーサイドやっております 金城 (@o0h_)です。
「腸よ鼻よ」を応援しています。人間ってたくましい。

腸よ鼻よ(1)

腸よ鼻よ(1)

webでも読めます https://ganma.jp/chohana

さて、掲題のとおりですが、以前にママリのマスプロモーションを実施した際の負荷対策として作成した機構をプラグインとして公開しました。

packagist.org

MySQL用のデータソースで、1つのモデル*1に対して複数のデータベース接続を提供する機構です。 例えば「参照しか走らないリクエストは参照用DBに接続して、更新系はマスターDBに接続する」といったような使い方を想定しています。

簡単に利用方法を紹介します。

  1. Pluginを配置する
  2. 「複数の接続先」をconfig(database.php)に書き込む
  3. (Controller等で)必要に応じて接続先情報を変更するメソッドを実行する

Pluginの配置

Composerによるinstallに対応しています。
これを、plugins もしくは app/Plugin に配置してください。 詳細はCookBookを参考にしてください

https://book.cakephp.org/2.0/ja/plugins/how-to-install-plugins.html#composer また、Pluginを配置した後、プラグインをloadするのをお忘れなく。

接続情報の設定

DATABASE_CONFIGの設定を変更します。 例として、書き込み対応の「master」参照専用の「secondary」という、2つのDB接続を利用するケースを想定します。

  • masterは host:master-db-host; port: 3306; login: root; pass: password
  • secondaryは host:secondary-db-host; port: 3306; login: root; pass: password
  • DB名はどちらも my_app
  • デフォルトでは secondary に接続を向ける

その際には、以下のような記述になります。

<?php
class DATABASE_CONFIG {
    public $default = array(
        'datasource' => 'MasterReplica.Database/MasterReplicaMysql',
        'connection_role' => 'secondary',
        'connections' => array(
            '_common_' => array(
                'login' => 'root',
                'pass' => 'password',
                'database' => 'my_app',
            ),
            'master' => array(
                'host' => 'master-db-host',
            ),
            'secondary' => array(
                'host' => 'secondary-db-host',
            ),
        ),
    );
}
  • まず、datasourceを MasterReplica.Database/MasterReplicaMysql に変更します。
    app/Plugin/MasterReplica にプラグインを配置している想定です
  • connection_role に、「デフォルトでどこに接続させるか」という名前を指定します
  • connections という配列で、「接続名 = key」として、それぞれの接続情報を設定します
    • _common_ は特別なkeyで、全接続先に対する共通設定値をもたせます。今回は login, pass, databaseが共通です
    • master, secondaryに対して「個別に設定したい情報」をもたせます。この内容が、 _common_ と結合されて利用されます
    • なお、接続名(master, secondary)には任意の名前を利用することが可能です

これで接続情報のセットアップができました。

実際に接続先情報を変更する

Datasource経由で、「接続先変更」メソッドを実行することで、接続先の変更ができます。 switchConnectionRole($接続先名) を利用してください。

例えば、以下のコードは実際にプラグインのテストコードとして利用しているスニペットをベースにしたものです。 cf: MasterReplicaMysqlIntegrationTest.php#L78-L95

<?php

$conn = $this->Post->getDataSource();

// masterに向けて書き込みを行う
$conn->switchConnectionRole('master');
$newData = [
    'user_id' => 10,
    'title' => 'Lorem ipsum dolor sit amet',
    'content' => 'Lorem ipsum dolor sit amet',
];
$this->Post->save($newData);

// secondaryに向ける
$conn->switchConnectionRole('replica');

$savedData = $this->Post->read();

実用例①

「ログインしているユーザーはmasterに向ける(データの更新が可能)」という場合は、AppController::beforeFilter()で操作するというのも手です。

<?php

// ログイン処理後
$role = AuthComponent::user() ? 'master' : 'replica';
MasterReplicaMysql::setDefaultConnectionRole($role);

実用例②

IntegrationTest実行時に副作用として「接続先が切り替わったまま」になるので注意してください。
具体的に言うと、「参照DBに向けたままだとtableの作成やtruncateに失敗する」ので、setUp()/tearDown()ができません。

ControllerTestCaseを継承したサブクラスを用意し、 testAction() メソッドを上書きすることで対応が可能です。

<?php

protected function _testAction($url, $options = []) {
        $sourceList = ConnectionManager::sourceList();
        foreach ($sourceList as $source) {
            $obj = ConnectionManager::getDataSource($source);
            if (get_class($obj) !== 'MasterReplicaMysql') {
                continue;
            }
            if (in_array($obj, $this->masterReplicaSources, true)) {
                continue;
            }
            $this->masterReplicaSources[] = $obj;
            // DB再接続時にテーブル情報のキャッシュが副作用をもたらすケースがあるので
            // キャッシュ利用フラグを折っておく
            $obj->cacheSources = false;
        }

        try {
            $result = parent::_testAction($url, $options);
        } finally {
            // _testAction内での副作用を消すため、
            // 初期接続先をmasterにした上でDB接続を初期化する
            if ($this->masterReplicaSources) {
                MasterReplicaMysql::setDefaultConnectionRole('master');
                foreach ($this->masterReplicaSources as $obj) {
                    $obj->switchConnectionRole('master');
                }
            }
        }

        return $result;
}

CakePHP2のプラグイン作成にあたって

FriendsOfCake/travis が便利で、活用させていただきました。

github.com 一部、(Cake3がメインストリームということで・・)工夫する必要がある箇所もありますが、それでも.travis.ymlをかなり簡潔に保てて利便性が高いと思います。

詳しい内容は 実際のYAMLファイルを御覧ください。

おわり!

かなり足早にではありますが、簡単なご紹介をさせていただきました。
PRやIssueをお待ちしております!!!!💪💪💪 簡単な質問等であれば、お気軽にTwitterでリプ飛ばしてください。

コネヒトではCakePHP3も同様の機構を利用しており、こちらも現在OSS化作業を進めているところです。
近日中に披露できれば、と思います。

*1:databaseのconnection定義