fuelphpで複数DBに対応する方法 - 大規模サービスも想定して

やりたいこと

ある程度の規模のサービスや構成上db自体を分けた方が効率よく開発、運用できることがあるかと思います。
その際にfuelphpではどのように開発するか考えてみる

想定している構成
・マスタdbA
・スレーブdbA
・マスタdbB
・スレーブdbB

db設定

今回は開発環境を想定して、fuel/app/config/development/db.phpを編集します。

<?php
return array(
    // マスタdbA
    'masterA' => array(
        'connection'  => array(
          'dsn' => 'mysql:host=masterA_hostname;dbname=dbname',
          'username'   => 'master_userame',
          'password'   => 'master_password',
        ),
        'readonly' => array('slave1'),
    ),
    // スレーブdbA
    'slaveA' => array(
      'type' => 'pdo',
      'connection'  => array(
        'dsn' => 'mysql:host=slaveA_hostname;dbname=dbname',
        'username'   => 'slave_username',
        'password'   => 'slave_password',
       ),
    ),
    // マスタdbB
    'masterB' => array(
        'connection'  => array(
          'dsn' => 'mysql:host=masterB_hostname;dbname=dbname',
          'username'   => 'master_userame',
          'password'   => 'master_password',
        ),
        'readonly' => array('slaveB1'),
    ),
    // スレーブdbB
    'slaveB' => array(
      'type' => 'pdo',
      'connection'  => array(
        'dsn' => 'mysql:host=slaveB_hostname;dbname=dbname',
        'username'   => 'slave_username',
        'password'   => 'slave_password',
       ),
    ),
);

ほとんどそのままですが、通常だと「readonly」は1カ所のみで定義していたのを「masterB」でも別途定義します。

実装例

共通dbクラス

<?php
class Model_Common extends Model
{
    // 参照系の例 dbAのスレーブへアクセス
    public static function getOne($query,$params,$is_master=false,$db="masterA")
    {
        try {
            $db = \Database_Connection::instance($db, 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());
        }
    }
}

こんな感じで引数でdbを指定する事で振り分けが可能です。
で、実際に振り分ける処理を考えてみます。よくありそうな形で考えてみます

テーブルによってdbをわける

基本的なテーブルとログ系のテーブルを分けるときなど。
あまりにも用途が違うテーブルを別dbにもたせる実装はけっこうあると思います。
このよう場合に考えられるかと思います。

実装例:これは簡単でテーブルごとにModelを用意してそれぞれにdb名をもたせてしまうがわかりやすいのでこのように実装しています。

Model_Hoge extends Model_Common
{
    static $db = "masterA";
    public static function getByid($id)
    {
        $query = "select * from hoge where id = :id";
        $params = array("id"=>$id);
        return parent::getOne($query,$params,$is_master=false,self::$db);
    }
}
ドメイン(サービス)によってdbをわける

1つのエンジンで複数のサービスを運営する場合などに考えられると思います。
基本的なプログラムは同じなんだけど、商品の出し分け等はdbに依存する部分かと思われるのでドメイン等で参照先のdbをわける方法になります。

実装例:色々やり方はあるかとは思いますが、共通のコントローラなどでドメインを判別してConfig::setでdb名を定義してしまうのがシンプルだと思います。

// サービス共通のコントローラ
Controller_Common extends Controller
{
    if (Input::server('SERVER_NAME') == 'a.example.com') {
      Config::set('db', 'masterA');
    } else if (Input::server('SERVER_NAME') == 'b.example.com') {
      Config::set('db', 'masterB');
    }
}

Model_Hoge extends Model_Common
{
    public static function getById($id)
    {
        $query = "select * from hoge where id = :id";
        $params = array("id"=>$id);
        return parent::getOne($query,$params,$is_master=false,Config::get("db"));
    }
}
ユーザによってdbをわける

ソーシャルゲーム等ではどうしても更新系の処理が多くなるのでマスタ自体を分割する実装を検討する場合があると思います。

実装例:これはdb自体がユーザによって動的にかわってしまうので、db名を管理するクラスを1つ用意してしまうのがわかりやすいと思います。

Model_DbMgr extends Model_Common
{
    public static function getDbNo($user_id)
    {
        // なんらかのルールで$noを決める
        // $user_idの下1桁などなど
        return $no;
    }
}

Model_Hoge extends Model_Common
{
    public static function getById($user_id)
    {
        $query = "select * from hoge where user_id = :user_id";
        $params = array("user_id"=>$user_id);
        return parent::getOne($query,$params,$is_master=false,ModelDbMgr::getDbNo($user_id));
    }
}

まとめ

ざっくりでしたが、ざっと考えられる複数db構成のアプリの場合の実装を考えてみました。
fuelphpは大規模開発には向かないと聞いた事があるのですが、とりあえずdbの面だけで考えてみるとそれほど困らなそうな気がします。
大規模開発とかになると、負荷対策以外のことでも検討することが増えるのかもしれないのでそう簡単にはいかないのかも知れませんが、、、
あと、dbが増えたら増えた分定義を増やす必要があるのもなんか無駄な気がします、、、これはインフラ側の工夫で解決できそうですが

とりあえず思いつくところであげてみました。
以上です