かなり特殊な要件ですが、すごくハマりましたのでメモ。
要件
ユーザーに対し、DBが1つ。そのため、ユーザーが増えるとDBが増える仕様。
その中で、とあるModelで別Modelを使用しないといけない時に、うまくいかなくてハマりました。
詳細はDBを切り替えたはずなのに、何故かModelは切り替える前のDBを参照している。という事象。
また、とある方法を使えばうまくいきましたが、DBの切り替えが3ユーザー以上になるとうまくいかなくなりました。
試したこと
DATABASE_CONFIG
の dataSource
を動的に追加
~/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
が入っています。
なぜこのような処理を記載しているかわかりませんが、とりあえずは切り替え前のdefault
のprefix
が必要になるってことですね。
んで、④.終わったら、動的に追加したdataSourceを削除(other_db_one)、切り替え前に戻します(default)
見づらいので並べます。
- dataSourceを追加(other_db_one)
- 他modelをinit()
setDataSource()
- 終わったら、動的に追加した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.php
とClassRegistry
の境目がいまいちよくわかっていないので、どこからどこまで切り替わったのかは今度調べてみる。