読者です 読者をやめる 読者になる 読者になる

ITの隊長のブログ

ITの隊長のブログです。いや、まだ隊長と呼べるほどには至っていないけど、日々がんばります。CakePHPとPlayFrameworkを使って仕事しています。最近はAngular2をさわりはじめたお(^ω^ = ^ω^)

【CakePHP】DBを切り替えながらとあるModelで、別Modelを使用するときのメモ

CakePHP PHP

スポンサードリンク

photo by koyhoge

かなり特殊な要件ですが、すごくハマりましたのでメモ。

要件

ユーザーに対し、DBが1つ。そのため、ユーザーが増えるとDBが増える仕様。

その中で、とあるModelで別Modelを使用しないといけない時に、うまくいかなくてハマりました。

詳細はDBを切り替えたはずなのに、何故かModelは切り替える前のDBを参照している。という事象

また、とある方法を使えばうまくいきましたが、DBの切り替えが3ユーザー以上になるとうまくいかなくなりました。

試したこと

DATABASE_CONFIGdataSource を動的に追加

~/app/Config/database.phpのメンバ変数として登録されるDBの設定を動的に増やしました。

<?php

class AppModel extends Model {

 /**
   * 動的に追加したDBに変更する
   * 
   * @param type $user
   * @param type $password
   * @param type $dbName
   */
  public function changeConnection($user, $password, $dbName) {
    $getConnection = $this->getDbConnection('127.0.0.1', $user, $password, $dbName);
    $this->changeDataSource($dbName, $getConnection);
  }

  /**
   * データソースの作成
   * 
   * @param type $dbName
   * @param type $dbConnection
   */
  public function changeDataSource($dbName, $dbConnection) {
    ConnectionManager::create($dbName, $dbConnection);
    $this->changeDatabase($dbName);
  }

  /**
   * 動的に追加したデータソースの削除
   */
  public function dropChangeDataSource($dbName) {
    ConnectionManager::drop($dbName);
    $this->changeDatabase();
  }

  /**
   * データベースを変更する
   * 空の場合はもとのDBへ戻す
   * @param string $dbName
   */
  public function changeDatabase($dbName = null) {
    if (empty($dbName)) {
      // ここは任意で変更してください
      $dbName = 'default';
    }
    $this->useDbConfig = $dbName;
  }

  /**
   * dbコネクション情報を返す
   * 
   * @param type $host
   * @param type $user
   * @param type $password
   * @param type $dbName
   */
  private function getDbConnection($host, $user, $password, $dbName) {
    return array(
      'datasource' => 'Database/Mysql',
      'persistent' => false,
      'host'       => $host,
      'login'      => $user, 
      'password'   => $password,
      'database'   => $dbName,
      'prefix'     => '',
      'encoding'   => 'utf8',
    );
  }
}

わかりづらいかもしれませんが、changeConnection()に、接続したいDBの情報を渡して dataSource を作成します。

いらなくなったら、dropChangeDataSource()で削除したらおkです。

この方法を使用すると切り替えはうまくいきました。

が。

<?php

class Hoge extends Model {

  public function hoge() {
    $modelHuga = ClassRegistry::init('huga');
    // これは切り替わっていない接続
    return $modelHuga->find('all');
  }
}

上記のコードで他 Model を検索しても切り替え前の DB を参照してしまい、うまくいきませんでした。

よくわかっていませんが、ClassRegistry::init('huga');が、staticでアクセスしているのが原因(?というか俺の認識漏れ?)ということがわかりました。

じゃあ、newにしちゃえ

ClassRegistry自体がよくわかっていませんが、静的にアクセスしているのなら、動的にしちゃえって思いましたので、こうしました。

<?php

App::uses('Huga', 'Model');

class Hoge extends Model {

  public function hoge() {
    $modelHuga = new Huga();
    return $modelHuga->find('all');
  }
}

しかし、これもダメでした。恐らくApp::uses('Huga', 'Model');が、最初の実行時で切り替え前のModelをロードしているからかなと思っています。

色々ハマって、とりあえずはできた。

色々調べたら、とりあえずこうするとできました。参考のサイトがClassRegistryだったので、戻す。

<?php

App::uses('Huga', 'Model');

class Hoge extends Model {

  public function hoge($dataSource) {
    $modelHuga = ClassRegistry::init('Huga');
    $modelHuga->setDataSource($dataSource);
    return $modelHuga->find('all');
  }
}

$modelHuga->setDataSource($dataSource);ってのが、追加されていますが、これは$modelHugaの、dataSourceを変更してあげています。そうすると切り替えて他Modelにアクセスできるようになりました。

が、しかし・・・・・。

全体を見てみましょう。

<?php

App::uses('Huga', 'Model');

class Hoge extends Model {

  public function hoge($dataSource) {
    $modelHuga = ClassRegistry::init('Huga');
    $modelHuga->setDataSource($dataSource);
    return $modelHuga->find('all');
  }

  public function changeDbAfterOtherModelAccess() {
    // 追加してアクセスしたいデータベース
    $dbConfigList = array('other_db_one', 'other_db_two');

    // 現状のDB情報を取得
    $fields     = get_class_vars('DATABASE_CONFIG');
    $existingDb = $fields['default'];

    foreach($dbConfigList as $v) {
      try {
        // DB切り替え
        $this->changeConnection($existingDb['login'], $existingDb['password'], $v);

        // 切り替え後の処理
        $data = $this->hoge($v);

        // 追加したdataSourceを削除
        $this->dropChangeDataSource($v['sub_domain']);
      } catch (Exception $e) {
        $this->log($e->getMessage());
        // 追加したdataSourceを削除
        $this->dropChangeDataSource($v['sub_domain']);
      }
    }
  }
}

アクセスしたいDB名と作成する dataSource は一緒の名前になります。

上記コードを実行した際、1回のother_db_oneのアクセスは成功しますが、2回目のother_db_twoで、失敗します。

理由としては、$modelHuga->setDataSource($dataSource);の中が、状態を持っていたからでした。

詳細は下記コードでエラーが発生していました。

  • ~/lib/Cake/Model/Model.php 3531行目ぐらい
<?php

/**
 * Sets the DataSource to which this model is bound.
 *
 * @param string $dataSource The name of the DataSource, as defined in app/Config/database.php
 * @return void
 * @throws MissingConnectionException
 */
  public function setDataSource($dataSource = null) {
    $oldConfig = $this->useDbConfig; // ここの$this->useDbConfig が、状態を持っている

    if ($dataSource) {
      $this->useDbConfig = $dataSource;
    }

    $db = ConnectionManager::getDataSource($this->useDbConfig);
    if (!empty($oldConfig) && isset($db->config['prefix'])) {
      $oldDb = ConnectionManager::getDataSource($oldConfig); // ここでエラーが発生!!

      if (!isset($this->tablePrefix) || (!isset($oldDb->config['prefix']) || $this->tablePrefix === $oldDb->config['prefix'])) {
        $this->tablePrefix = $db->config['prefix'];
      }
    } elseif (isset($db->config['prefix'])) {
      $this->tablePrefix = $db->config['prefix'];
    }

    // ~ 省略 ~

ちとわかりにくいが、①.dataSourceを追加(other_db_one)、②.他modelをinit()、③.setDataSource()を実行したときに上記コードの、$oldConfig = $this->useDbConfig。これに入る値は切り替える前なので、defaultが入っています。

なぜこのような処理を記載しているかわかりませんが、とりあえずは切り替え前のdefaultprefixが必要になるってことですね。

んで、④.終わったら、動的に追加したdataSourceを削除(other_db_one)、切り替え前に戻します(default)

見づらいので並べます。

  1. dataSourceを追加(other_db_one)
  2. 他modelをinit()
  3. setDataSource()
  4. 終わったら、動的に追加したdataSourceを削除(other_db_one)、切り替え前に戻します(default)

この処理をユーザーDB毎にループさせます。しかし問題の2回目(other_db_two)にて、③の処理でエラーが発生しました。

その時のModel.phpデバッグすると$this->useDbConfigが、other_db_oneを持っていました。しかし、other_db_oneは1回目のループの処理④で削除しているので、アクセスすることができません。そのため、$oldDb = ConnectionManager::getDataSource($oldConfig);でエラーが発生しました。

ややこしいですが、なんとなく理解はできます。

しかしお気づきだろうか。

AppModel.phpに追記したコードで

<?php

  /**
   * データベースを変更する
   * 空の場合はもとのDBへ戻す
   * @param string $dbName
   */
  public function changeDatabase($dbName = null) {
    if (empty($dbName)) {
      // ここは任意で変更してください
      $dbName = 'default';
    }
    $this->useDbConfig = $dbName;
  }

このコードを追記していますが、先ほどの4番目の処理で$this->useDbConfig = $dbName;を実行しているはずなんです。削除するときは切り戻すだけなので、dataSourceはdefaultになっているはずですが、何故か先ほどのModel.phpでは1回目のループでsetDataSource()で渡したdataSource名が入っていました。

この辺がよくわかりません。あれ?AppModel.phpってModel.phpを継承していませんでしたっけ? 同じ状態を持っているんじゃないのか。

ClassRegistryって、一体何なんだ・・・・?

謎は深まるばかりですが、とりあえずは現在用意されているdataSourceが存在しているのなら、エラーは起きないことはわかっていましたので、こうしました。

<?php

  public function hoge($dataSource) {
    $modelHuga = ClassRegistry::init('Huga');
    $modelHuga->setDataSource($dataSource);
    $data = $modelHuga->find('all');
    $modelHuga->setDataSource('default'); // ここでdefaultに戻す
    return $data;
  }

・・・・・

できたけど、なんかいやだなぁ。dataSourceを一旦動的に作成して、ここでも切り替えて、んで切り戻して、削除して・・・めんどくさい。

結局はこうしました

色々試行錯誤したところ、1行だけコードが減ったこっちにしました。(それでも。。。一行)

<?php

App::uses('Huga', 'Model');

class Hoge extends Model {

  public function hoge($dataSource) {
    $modelHuga = new Huga();
    $modelHuga->setDataSource($dataSource);
    return $modelHuga->find('all');
  }
}

newで作成したModel Objectで、setDataSource()を実行すると切り替え前の$useDbConfigが必ずdefaultになるので、defaultを削除しない限りエラーはでることはないでしょう。

しかし、、、どうせならDB切り替えたら、そのまま全部切り替わればいいのに。。。まぁ何か理由があるんでしょうけど。

んで、結局のところ、AppModel.phpClassRegistryの境目がいまいちよくわかっていないので、どこからどこまで切り替わったのかは今度調べてみる。