今回はちょっと入門者寄りです。
YiiのActiveRecordはSQLの抽象化をやりすぎないのが嬉しいところです。コードからSQLが想像できるので、データ抽出のパフォーマンスチューニングするとき柔軟なので、プラグマティックで素敵です。ですがその反面、抽象度の高いORMで期待できる、インスタンスの高度な自動管理がありません。
データの関係を定義する relations()
メソッドのオーバーライドですが、これ、データの抽出にはすごく使えるのに、永続化のときはまるっきり使えません。複雑なオブジェクトの永続化では、関係モデルのオブジェクト構造の個々のインスタンスとその関係を、いちいち個別に保存する必要があります。
ここ、初めての人がまず落ちる落とし穴なので、しつこいぐらいで考え方を説明しますね。
HAS_MANYとBELONGS_TOの場合
例: アイテムはいずれかのコンテナに入っている
CREATE TABLE container (
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
...
);
CREATE TABLE item (
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
container_id INTEGER NOT NULL KEY REFERENCES container(id),
...
);
- Container HAS_MANY Item
- Item BELONGS_TO Container
こういうのの関係を保存したいときは、 アイテムの外部キーカラムの値を直接書き換え ます。
冗談じゃないですよ。マジですよ。YiiのActiveRecordはSQLをぜんぜん隠蔽しないのです。
<?php
$container = new Container;
$container->save();
$item = new Item;
$item->container_id = $container->id; // itemテーブルにはcontainer_id列がある
$item->save();
以下のような方法では関係が保存されません。
<?php
// これはダメ
$container = new Container;
$container->items[] = new Item;
$container->save(); // これはINSERT一発だけ
// テーブルに列がないからitemsは無視される
// これもダメ
$item = new Item;
$item->container = $container;
$item->save(); // これでINSERT一発だけ
// テーブルに列がないからcontainerは無視される
ポイントはこんな感じです。
- 連鎖的に暗黙のINSERT/UPDATE文が発生すると期待しない
- arrayやオブジェクトを指すプロパティはだいたい無視される
また、外部キーの値を保存しただけなので、保存の直後はモデルのインスタンス間に関係が貼られません。もしビューやテストでこの新規モデルのインスタンス関係をもう一度使いたいと思った場合は、データベースにのみ作られた関係をもとにして、モデルをリフレッシュする必要があります。
<?php
// いろいろ save() とか
$container->refresh();
$item->refresh();
var_dump($container->items);
// これでようやくItemのarrayが見える
var_dump($item->container);
// これでようやく親Containerが見える
ええと、refresh()
がやってることは、乱暴にいうと要するにSELECTしなおしです。
こんなの我慢出来ないという方へ
もしどうしても、「オブジェクトだけで関係を作って、それを一気に保存したい」というなら、Yiiにはイベントがあります。
afterSave()
で保存のあと子の外部キーをすべて親を指すように書き換えて個々に永続化、とか、beforeSave()
で未保存の親だったらまずそれを永続化して親のIDを自分に割り当てから自分を保存、とか、そういうことをユーザごとに、自分のモデルがどういう設計なのかに応じて実装すればいいよ、ということです。
<?php
public function beforeSave()
{
$this->container_id = $this->container->id;
return true;
}
決まったイベントのセットを再利用したいなら CActiveRecordBehavior
がありますね。ビヘイビアを使えばコードがコピペになる心配もなし。よかったよかった。
ちなみに、そのへんを一般化しようとしているエクステンションがちらほらあります。でも、問題の原因がわからないからエクステンション任せにしちゃえ、というのはあまりお勧めしません。わからずに使うと余計複雑になって、知らないうちにひどいSQLが走っていたなんてことになる恐れもありますし。実装の参考として、一応紹介だけしておきます。
activerecord-relation-behavior
未知のコードを使って、まさかの全件取得で数万要素のarrayがドーン、とか、そういうのをやられないように注意してください。自分の場合は、MANY_MANYの中間テーブルを全消し/全入れされたことがあって、次から別のプロジェクトでは絶対に使わないと心に決めました。
どこがActiveRecordやねん
ひどいじゃないか、どこがActiveRecordだよ、PHPerの作ったものなんて所詮こんなもんかよ。いやいや、怒るのはちょっと待って。
抽象度の高いORMは確かに美しくてAPIにノイズが少ないんだけど、それってどういう実装になってるんでしょうか。そういうのは、エンジン内のキャッシュに独自のオブジェクトデータベースを作って、それを必要な時だけRDBMSと同期するようなアーキテクチャになっています。
そんな高級なORMでは、SQLを意図通り制御するのが難しくなります。いざというとき、SQLを直接操作してチューニングできるのがYiiのいいところ、ですよね。
※ 昔JavaでHibernateを触っていたんですが、あれ、HQLというSQLに似た別の言語を持っていて、それでキャッシュとやりあう感じでした。本当のSQLレベルのチューニングは、モデルのコードからではなく、マッピング定義でしかできません。もし「この機能だけどうしてもSQLで考えないとできない」っていうのがあると、モデルの実装コードでは済まなくなって、こっそり仕事を終わらせられなくなります。
何より、高品質を目指せば目指すほどエンジンが大きくなるので、その初期化オーバーヘッドが、PHPの実行モデルでは不利になってきます。コンパイルと初期化を大幅に前倒しできる、Javaみたいな環境とは違うので。(だからこそ逆にPHPは継続的なリリースに向いてるのですよ)
それに、Webのアプリケーションでは、ほとんどの場合、「ひとつのリクエスト内では親のidをPOSTして関係の変更を保存のみ。できたらリダイレクト」「リダイレクト先は別のリクエストなのでフレッシュな状態でSELECTから」というような作りになってきます。なので、常にORMエンジンが矛盾のない状態を維持するような機能があっても、無駄になることが多のではないでしょうか。
そういう、0.9を0.99にするような高級さを指向するのは、安い速い美味いのYiiっぽくないですよね。
アクティブレコードを使いすぎないでください。アクティブレコード は OOP 流にデータをモデリングするには便利ですが、クエリ結果に対して一つまたは複数のオブジェクトを作る必要があるため、パフォーマンスを低下させます。膨大なデータを扱うアプリケーションには、DAO を使うか、直接データベース API を使うのが賢明な選択でしょう。
そうそうこれこれ。
でも、もしどうしても合わない場合は、使わないという選択肢もあります。Yiiでもっとも有名なとあるプロダクトでは、標準のORMからRedBeanに置き換えて、独自のフレームワークを作って開発されています。
というわけで、YiiのActiveRecordはあくまで、SQL関連の手続きをすごくうまくまとめたライブラリなんだ、というぐらいに思っておけば、期待し過ぎずにうまく使えるんじゃないでしょうか。
つぎ、MANY_MANYの話に続く。