fuelphpでmysqlのレプリケーションに対応する方法

やりたいこと

少しアクセスの多いサイトなどではありがちなmysqlレプリケーションを使うことがあると思います。
fuelphpでそれに対応するときの作業をメモしておきます。
いちおう今回想定する環境はマスタdb1台、スレーブdb2台構成のものとします。

db定義を設定

とりあえず今回は開発環境(development)での作業を扱います。
まずは、fuel/app/config/development/db.phpを編集

<?php
return array(
    // マスタdb
    'default' => array(
        'connection'  => array(
          'dsn' => 'mysql:host=master_hostname;dbname=dbname',
          'username'   => 'master_userame',
          'password'   => 'master_password',
        ),
        'readonly' => array('slave1', 'slave2'), // ★これを忘れないように
    ),
    // スレーブdb
    'slave1' => array(
      'type' => 'pdo',  // ★ここも忘れないように
      'connection'  => array(
        'dsn' => 'mysql:host=slave1_hostname;dbname=dbname',
        'username'   => 'slave_username',
        'password'   => 'slave_password',
       ),
    ),
    'slave2' => array(
      'type' => 'pdo',  // ★ここも忘れないように
      'connection'  => array(
        'dsn' => 'mysql:host=slave1_hostname;dbname=dbname',
        'username'   => 'slave_username',
        'password'   => 'slave_password',
       ),
    ),
);

こんな感じになります。で、マニュアルみながら作業してたときにちょっとハマったのは2点です。
マスタdbの定義の箇所に「readonly」の項目を見落としたことです。定義するスレーブ分追加する必要があります。
もう1点はスレーブの方にもホスト情報、アカウント情報以外にもmysqlアクセスで使用するドライバ名を記述する必要があります。
defaultのものが引き継がれると勝手に思っていたのですが、これ必要です。
必要であれば文字コードの指定とかも必要なので追記してあげてください。

呼び出す方法

マスタ、スレーブdbへのクエリ実行方法

DB::query($query)->parameters($params)->execute()->as_array();  // マスタdbへ
DB::query($query)->parameters($params)->execute("slave1")->as_array();  // スレーブdb1へ
DB::query($query)->parameters($params)->execute("slave2")->as_array();  // スレーブdb2へ

調べてみるとこんな感じで振り分けが可能です。
が、slave1とslave2への振り分けってアプリ側でやるの?という疑問が。
で、ソースみてみるとランダムに振り分けてくれる様な実装に見えたのがですがなんか上手くいかず、、、
たぶんorm使ってる場合には更新系クエリはマスタdbへ、参照系クエリはスレーブdbへ自動で振り分けてくれる様な実装になってそうでした。(すみません、実際に試してはいません)

で、僕の場合は基本的にormは使わないでModelにsqlを直接書いてるのでこれだと自前で振り分ける実装が必要なのかと思っていたのですがfuelのcoreクラスを使えばなんとかなりそう。

dbの共通クラスを作成する

とりあえず以下の様なdbアクセスクラスを用意しました。sqlの実行は必ずこのクラスを経由するようにします。
ログ出力の箇所とかは僕の趣味です。共通dbクラスについてざっくりしか書きませんが、こちらにもまとめてみましたので興味があればみてみてください
fuelphpで必ずやってる設定などまとめ - とりあえずphpとか

共通dbクラス

<?php
class Model_Common extends Model
{
    // 参照系の例
    public static function getOne($query,$params,$is_master=false)
    {
        try {
            $db = \Database_Connection::instance($db=null, null, !$is_master); // ★スレーブ用にコレを追加
            $result = DB::query($query)->parameters($params)->execute($db)->as_array();
            Log::debug('sqlデバッグ::'.DB::last_query());
            return empty($result) ? array() : $result[0];
        } catch (Exception $e) {
            Log::error('sqlエラー::'.$e->getMessage());
            throw new HttpServerErrorException($e->getMessage());
        }
    }

    // 更新系の例
    public static function execute_insert($table,$data)
    {
        try {
            $r =DB::insert($table)->columns(array_keys($data))->values(array_values($data))->execute();
            Log::debug('sqlデバッグ::'.DB::last_query());
            return $r[0];
        } catch (Exception $e) {
            Log::error('sqlエラー::'.$e->getMessage());
            throw new HttpServerErrorException($e->getMessage());
        }
    }
}

sql実行するクラス

<?php
class Model_Users extends Model_Common
{
    // スレーブへ参照系クエリ
    public static function getById($id)
    {
        $sql = "select * from users where id = :id";
        return self::getOne($sql, array("id" => $id));
    }

    // マスタへ参照系クエリ
    public static function getByIdToMaster($id)
    {
        $sql = "select * from users where id = :id";
        return self::getOne($sql, array("id" => $id), $is_master=true);
    }
}

更新系のクエリはスレーブは関係ないので、意識するのは参照系のクエリのみです。
Model_Common::getOneの引数でマスタとスレーブを指定できるように実装しています。

ちょっと長くなりましたが、こんな感じかなと思います。
fuelphpのdb回りではもう1点、作っておきたいものがあって1アプリの中に複数dbある場合どのように実装するかもまとめておきたいと思います。
イメージとしてはマスタAとスレーブA1・A2があってさらにマスタBとスレーブB1・B2があるような形です。

以上です