mirror of
https://github.com/yiisoft/yii.git
synced 2026-03-06 16:16:53 +01:00
718 lines
36 KiB
Plaintext
718 lines
36 KiB
Plaintext
リレーショナルアクティブレコード
|
|
========================
|
|
|
|
単一のデータベーステーブルからデータを選択するためにアクティブレコード (AR) を使う方法を見てきました。
|
|
この章では、AR を使って、いくつかの関係するデータベーステーブルをつなげ、結合されたデータセットを取得する方法を示します。
|
|
|
|
リレーショナル AR を使う場合は、結合すべきテーブルに主キー・外部キー制約が宣言されていることが推奨されます。
|
|
この制約がリレーショナルデータの一貫性と整合性を保持するために役立ちます。
|
|
|
|
分りやすくするために、この章では例題として、以下のエンティティ関係 (ER) 図に示されるデータベーススキーマを使用します。
|
|
|
|

|
|
|
|
> Info|情報: 外部キー制約のサポートは DBMS 毎に異ります。
|
|
> SQLite 3.6.19 またはそれ以前のものは外部キー制約をサポートしませんが、テーブルを作成する際に制約を宣言することが出来ます。
|
|
> MySQL の MyISAM エンジンは外部キーを全くサポートしません。
|
|
|
|
|
|
リレーションの宣言
|
|
----------------------
|
|
|
|
AR のリレーショナルクエリを使用する前に、AR に対して他の AR クラスとどのように関係しているかを知らせる必要があります。
|
|
|
|
二つの AR クラスのリレーションは、AR クラスによって表現されるデータベーステーブルのリレーションと直接関係しています。
|
|
データベースの観点からは、二つのテーブル A と B の関係には、3つのタイプがあります。
|
|
1対多 (例えば `tb_user` と `tbl_post`)、1対1 (例えば `tbl_user` と `tbl_profile`)、多対多 (例えば `tbl_category` と `tbl_post`)。
|
|
ARでは、以下の4種類のリレーションがあります。
|
|
|
|
- `BELONGS_TO`: テーブル A と B の関係が1対多ならば、B は A に属しています (例 `Post` blongs to `User`)。
|
|
|
|
- `HAS_MANY`: 同じくテーブル A と B の関係が1対多ならば、A は多くの B を持っています (例 `User` has many `Post`)。
|
|
|
|
- `HAS_ONE`: これは A がたかだか一つの B を持っている `HAS_MANY` の特例です (例 `User` has at most one `Profile`)。
|
|
|
|
- `MANY_MANY`: これはデータベースにおいて多対多の関係と対応します。
|
|
多対多の関係は、1対多の関係に分割するために、関連付け用のテーブルが必要になります。
|
|
なぜなら大部分の DBMS は、直接には多対多の関係をサポートしないためです。
|
|
例題のデータベーススキーマでは、`tbl_post_category` がこの目的のために使用されます。
|
|
AR 用語では、`BELONGS_TO` と `HAS_MANY` の組合せとして、`MANY_MANY` を説明することができます。
|
|
例えば `Post` は多くの `Category` に属しています。そして `Category` には多くの `Post` があります。
|
|
|
|
AR でのリレーション宣言は、[CActiveRecord] クラスの [relations()|CActiveRecord::relations] メソッドをオーバライドすることで行います。
|
|
このメソッドはリレーション構成の配列を返します。
|
|
各々の配列要素は以下のフォーマットで示す一つのリレーションを意味します。
|
|
|
|
~~~
|
|
[php]
|
|
'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', ...付加オプション)
|
|
~~~
|
|
|
|
ここで `VarName` はリレーションの名前です。
|
|
`RelationType` はリレーションのタイプを指定します。
|
|
そしてそれは4つの定数、`self::BELONGS_TO`, `self::HAS_ONE`, `self::HAS_MANY`, `self::MANY_MANY` のうちの一つです。
|
|
`ClassName` はこの AR クラスに関係する AR クラスの名前です。
|
|
`ForeignKey` はリレーションに関係する外部キーを指定します。
|
|
そして各リレーションについて、最後に付加オプションを指定することができます (後で説明します)。
|
|
|
|
以下のコードでどのように `User` クラスと `Post` クラスのリレーションを宣言するかを示します。
|
|
|
|
~~~
|
|
[php]
|
|
class Post extends CActiveRecord
|
|
{
|
|
......
|
|
|
|
public function relations()
|
|
{
|
|
return array(
|
|
'author'=>array(self::BELONGS_TO, 'User', 'author_id'),
|
|
'categories'=>array(self::MANY_MANY, 'Category',
|
|
'tbl_post_category(post_id, category_id)'),
|
|
);
|
|
}
|
|
}
|
|
|
|
class User extends CActiveRecord
|
|
{
|
|
......
|
|
|
|
public function relations()
|
|
{
|
|
return array(
|
|
'posts'=>array(self::HAS_MANY, 'Post', 'author_id'),
|
|
'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'),
|
|
);
|
|
}
|
|
}
|
|
~~~
|
|
|
|
> Info|情報: 外部キーは2個以上のカラムで構成される複合キーでもかまいません。
|
|
この場合は、外部キーのカラムの名前をカンマで区切って結合した文字列か、または array('key1','key2') のような配列を使わなければなりません。
|
|
標準的でない PK->FK 結合を指定する必要があるときは、array('fk'=>'pk') として定義することが出来ます。
|
|
複合キーの場合は、array('fk_c1'=>'pk_c1','fk_c2'=>'pk_c2') となります。
|
|
`MANY_MANY` のリレーションにおいては、外部キーとして、関連付けテーブル名も指定されなければなりません。
|
|
例えば、`Post` における `categories` リレーションは、`tbl_post_category(post_id, category_id)` という外部キーにより指定されます。
|
|
|
|
AR クラスにおいてリレーションを宣言すると、各々のリレーションを表わす暗黙のプロパティがクラスに加えられます。
|
|
リレーショナルなクエリが実行された後、対応するプロパティは関連する AR の (単一または複数の) インスタンスで満たされます。
|
|
例えば、`$author` が `User` AR インスタンスを表している場合、`$author->posts` を使って、関連した `Post` インスタンスにアクセスすることが出来ます。
|
|
|
|
リレーショナルクエリの実行
|
|
---------------------------
|
|
|
|
リレーショナルクエリを実行する最も単純な方法は、AR インスタンスのリレーショナルなプロパティを読み出すことです。
|
|
プロパティが以前にアクセスされていない場合には、リレーショナルクエリが開始されます。
|
|
このクエリは二つの関係するテーブルを結合し、現行の AR インスタンスのプライマリキーでフィルタリングするものです。
|
|
そして、クエリの結果は、関連する AR の (単一または複数の) インスタンスとして、プロパティに保存されます。
|
|
これは **レイジーローディング (Lazy Loading)** アプローチとして知られており、リレーショナルクエリは関連するオブジェクトが最初にアクセスされて初めて実行されます。
|
|
以下の例は実際にこのアプローチをどのように使用するかを示します。
|
|
|
|
~~~
|
|
[php]
|
|
// ID が 10 である記事を取得
|
|
$post=Post::model()->findByPk(10);
|
|
// 記事の著者を取得。リレーショナルクエリはここで実行される
|
|
$author=$post->author;
|
|
~~~
|
|
|
|
> Info|情報: リレーションにより関連したインスタンスが取得できない場合、対応するプロパティは null または空の配列となります。
|
|
`BELONGS_TO` と `HAS_ONE` リレーションの場合結果は null です。
|
|
`HAS_MANY` と `MANY_MANY` では空の配列です。
|
|
`HAS_MANY` と `MANY_MANY` リレーションは、オブジェクトの配列を返すため、個々のプロパティにアクセスする前に、結果の配列をループする必要があることに注意してください。
|
|
そうでなければ、"Trying to get property of non-object (非オブジェクトのプロパティを取得しようとしている)" というエラーが発生します。
|
|
|
|
レイジーローディングアプローチは使うのに非常に便利ですが、それはいくつかの場合に効率的ではありません。
|
|
例えば `N` 個の著者情報にアクセスする場合、レイジーローディングアプローチを使うと `N` 個のジョインクエリを発行しなければなりません。
|
|
この状況ではいわゆる **イーガーローディング (Eager Loading)** アプローチをとる必要があります。
|
|
|
|
イーガーローディングアプローチでは、主となる AR インスタンスと共に関連する AR インスタンスを取得します。
|
|
これは、AR において [find|CActiveRecord::find] か [findAll|CActiveRecord::findAll] のいずれかと共に [with()|CActiveRecord::with] メソッドを用いることで達成されます。
|
|
例えば、
|
|
|
|
~~~
|
|
[php]
|
|
$posts=Post::model()->with('author')->findAll();
|
|
~~~
|
|
|
|
上記のコードは `Post` インスタンスの配列を返します。
|
|
レイジーアプローチとは異なり、`author` プロパティにアクセスする前に、各々の `Post` インスタンスの `author` プロパティは関連した `User` インスタンスを格納しています。
|
|
記事ごとにジョインクエリを実行する代わりに、イーガーローディングアプローチでは、一回のジョインクエリによって、すべての記事を著者と共に取得します。
|
|
|
|
|
|
複数のリレーション名を [with()|CActiveRecord::with] メソッド中で指定することができ、イーガーローディングアプローチでは一度で全ての情報を取得できます。
|
|
例えば、以下のコードは、著者とカテゴリーを付加して、すべての記事を取得します。
|
|
|
|
~~~
|
|
[php]
|
|
$posts=Post::model()->with('author','categories')->findAll();
|
|
~~~
|
|
|
|
イーガーローディングを入れ子で実行することもできます。
|
|
リレーション名のリストの代わりに、以下のようにリレーション名の階層的な表現を [with()|CActiveRecord::with] メソッドに渡します。
|
|
|
|
~~~
|
|
[php]
|
|
$posts=Post::model()->with(
|
|
'author.profile',
|
|
'author.posts',
|
|
'categories')->findAll();
|
|
~~~
|
|
|
|
上記の例は、著者とカテゴリーと共にすべての記事を取得します。
|
|
さらに各々の著者のプロフィールと記事を取得します。
|
|
|
|
イーガーローディングは、下記のように、[CDbCriteria::with] プロパティを指定しても実行することが出来ます。
|
|
|
|
~~~
|
|
[php]
|
|
$criteria=new CDbCriteria;
|
|
$criteria->with=array(
|
|
'author.profile',
|
|
'author.posts',
|
|
'categories',
|
|
);
|
|
$posts=Post::model()->findAll($criteria);
|
|
~~~
|
|
|
|
または
|
|
|
|
~~~
|
|
[php]
|
|
$posts=Post::model()->findAll(array(
|
|
'with'=>array(
|
|
'author.profile',
|
|
'author.posts',
|
|
'categories',
|
|
)
|
|
));
|
|
~~~
|
|
|
|
関連するモデルを取得しないリレーショナルクエリを実行する
|
|
---------------------------------------------------------
|
|
|
|
場合によっては、リレーショナルクエリを実行する必要があるけれども、関連するモデルは取得したくない、ということがあります。
|
|
たとえば、数多くの `Post` を投稿した `User` が沢山いるとしましょう。
|
|
記事は公開されている場合もあれば、下書き状態にとどまっている場合もあります。
|
|
これは `Post` モデルの `published` フィールドによって決定されます。
|
|
そして、公開されている記事を持っているユーザをすべて取得したいけれども、記事そのものには関心がない、という場合です。
|
|
これは以下のようにして達成することが出来ます。
|
|
|
|
~~~
|
|
[php]
|
|
$users=User::model()->with(array(
|
|
'posts'=>array(
|
|
// 記事は SELECT したくない
|
|
'select'=>false,
|
|
// けれども、公開されている記事を持つユーザだけを取得したい
|
|
'joinType'=>'INNER JOIN',
|
|
'condition'=>'posts.published=1',
|
|
),
|
|
))->findAll();
|
|
~~~
|
|
|
|
リレーショナルクエリのオプション
|
|
------------------------
|
|
|
|
既に述べたように、リレーションの宣言において追加のオプションを指定することが出来ます。
|
|
これらのオプションは、"名前-値" のペアとして指定され、リレーショナルクエリをカスタマイズするのに用いられます。
|
|
それらの概要は以下の通りです。
|
|
|
|
- `select`: 関連するARクラスのために選ばれるカラムのリスト。
|
|
デフォルトは '*' でありすべてのカラムを意味します。
|
|
このオプションで参照されるカラム名は曖昧さを無くさなければなりません。
|
|
|
|
- `condition`: `WHERE` 句です。
|
|
デフォルトは空で無条件を意味します。
|
|
このオプションで参照されるカラム名は曖昧さを無くさなければなりません。
|
|
|
|
- `params`: 生成された SQL 文にバインドされるパラメータ。
|
|
これは "名前-値" のペアの配列として与えられなければなりません。
|
|
|
|
- `on`: `ON` 句です。
|
|
ここで指定される条件は、`AND` オペレータ を使用して、JOIN の条件に追加されます。
|
|
このオプションで参照されるカラム名は曖昧さを無くさなければなりません。
|
|
このオプションは `MANY_MANY` リレーションには適用されません。
|
|
|
|
- `order`: `ORDER BY` 句です。
|
|
デフォルトでは空で無条件を意味します。
|
|
このオプションで参照されるカラム名は曖昧さを無くさなければなりません。
|
|
|
|
- `with`: このオブジェクトと共にロードすべき、子供のリレーションオブジェクトのリストです。
|
|
このオプションを不適切に使用すると、無限リレーションループが形成される可能性がありますので、注意してください。
|
|
|
|
- `joinType`: このリレーションのジョインタイプで、デフォルトでは `LEFT OUTER JOIN` です。
|
|
|
|
- `alias`: このリレーションと関連付けられたテーブルのエイリアスです。
|
|
デフォルトは null で、テーブルのエイリアスはリレーション名と同じであることを意味します。
|
|
|
|
- `together`: このリレーションと関連付けられたテーブルが、主テーブルおよびその他のテーブルとの結合を強制されるかどうかを決定します。
|
|
このオプションは `HAS_MANY` および `MANY_MANY` のリレーションでのみ意味があります。
|
|
このオプションが false にセットされた場合は、`HAS_MANY` または `MANY_MANY` のリレーションに関連付けられたテーブルは、メインの SQL クエリとは分離された SQL クエリの中で主テーブルと結合されます。
|
|
これは、そうする方が、重複して返されるデータが少なくなり、全体としてのクエリのパフォーマンスを向上させることが出来るからです。
|
|
このオプションが true にセットされた場合は、関連付けられたテーブルは常に、主テーブルがページ分割されても、単一の SQL クエリの中で主テーブルと結合されます。
|
|
そして、このオプションが何もセットされない場合は、主テーブルがページ分割されない場合に限って、関連付けられたテーブルが単一の SQL クエリの中で主テーブルと結合されます。
|
|
更なる詳細については、"リレーショナルクエリのパフォーマンス" を参照して下さい。
|
|
|
|
- `join`: 追加の`JOIN` 句です。
|
|
デフォルトでは空です。
|
|
このオプションは、バージョン 1.1.3 以降で利用可能です。
|
|
|
|
- `group`: `GROUP BY` 句です。デフォルトは空です。
|
|
このオプションで参照されるカラム名は曖昧さを無くさなければなりません。
|
|
|
|
- `having`: `HAVING` 句です。デフォルトは空です。
|
|
このオプションで参照されるカラム名は曖昧さを無くさなければなりません。
|
|
|
|
- `index`: 関連するオブジェクトを格納する配列のキーとして使われる値を持っているカラムの名前です。
|
|
このオプションを設定しない場合は、関連するオブジェクトの配列は 0 から始まる整数のインデックスを持ちます。
|
|
このオプションは、`HAS_MANY` および `MANY_MANY` のリレーションに対してのみ設定出来ます。
|
|
|
|
- `scopes`: 適用するスコープです。
|
|
単一のスコープの場合は `'scopes'=>'scopeName'` のように指定し、複数のスコープの場合は `'scopes'=>array('scopeName1','scopeName2')` のように指定する事が出来ます。
|
|
このオプションは、バージョン 1.1.9 以降で利用可能です。
|
|
|
|
さらに、以下のオプションは、レイジーローディングの際に、特定のリレーションのために利用できます:
|
|
|
|
- `limit`: 選択される行数の制限。
|
|
このオプションは `BELONGS_TO` リレーションには適用されません。
|
|
|
|
- `offset`: 選択される行のオフセット。
|
|
このオプションは `BELONGS_TO` リレーションには適用されません。
|
|
|
|
- `through`: 関連するデータを取得する際に、ブリッジとして使用されるモデルのリレーションの名前。
|
|
`HAS_MANY` および `MANY_MANY` のリレーションに対してのみ設定出来ます。
|
|
このオプションは、バージョン 1.1.7 以降で利用可能です。
|
|
|
|
以下では、`User` における `posts` リレーション宣言を修正して、上記のオプションのいくつかを含むようにしてみましょう。
|
|
|
|
~~~
|
|
[php]
|
|
class User extends CActiveRecord
|
|
{
|
|
public function relations()
|
|
{
|
|
return array(
|
|
'posts'=>array(self::HAS_MANY, 'Post', 'author_id',
|
|
'order'=>'posts.create_time DESC',
|
|
'with'=>'categories'),
|
|
'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'),
|
|
);
|
|
}
|
|
}
|
|
~~~
|
|
|
|
このように宣言した場合、`$author->posts` としてアクセスすると、著者の記事を作成日時の降順でソートして取得することが出来ます。
|
|
また、各記事のインスタンスには、そのカテゴリも付加されます。
|
|
|
|
カラム名の曖昧さを無くする
|
|
---------------------------
|
|
|
|
結合される二つ以上のテーブルに同じ名前のカラムが出現する場合、カラム名の曖昧さを無くする必要があります。
|
|
これは、カラム名の前にテーブルの別名 (エイリアス) を追加することで行います。
|
|
|
|
リレーショナル AR クエリにおいては、主テーブルの別名は `t` に固定されます。
|
|
一方、関連するテーブルの別名は、デフォルトでは、対応するリレーション名と同じものになります。
|
|
例えば、次の文では、`Post` および `Comment` の別名は、それぞれ、`t` および `comments` です。
|
|
|
|
~~~
|
|
[php]
|
|
$posts=Post::model()->with('comments')->findAll();
|
|
~~~
|
|
|
|
今、仮に、`Post` も `Comment` も、それぞれ、記事またはコメントの作成日時を示す `create_time` というカラムを持っているとします。
|
|
そして、記事とそれに対するコメントを一緒にして取得するときに、先ず記事の作成日時でソートし、次にコメントの作成日時でソートしたいとします。
|
|
このとき、`create_time` というカラム名の曖昧さを無くすために次のようにします。
|
|
|
|
~~~
|
|
[php]
|
|
$posts=Post::model()->with('comments')->findAll(array(
|
|
'order'=>'t.create_time, comments.create_time'
|
|
));
|
|
~~~
|
|
|
|
動的リレーショナルクエリオプション
|
|
--------------------------------
|
|
|
|
[with()|CActiveRecord::with] と `with` オプションの両方の場合とも、動的なリレーショナルクエリのオプションを使用することができます。
|
|
動的なオプションは、[relations()|CActiveRecord::relations] メソッドの中で指定された既存のオプションを上書きします。
|
|
たとえば、上記の `User` モデルにおいて、イーガーローディングのアプローチを使って、ある著者に属する記事を **昇順** (リレーション定義中の `order` オプションは降順です) で取得したいならば、以下のように行います。
|
|
|
|
~~~
|
|
[php]
|
|
User::model()->with(array(
|
|
'posts'=>array('order'=>'posts.create_time ASC'),
|
|
'profile',
|
|
))->findAll();
|
|
~~~
|
|
|
|
動的なクエリオプションは、レイジーローディングのアプローチを使ってリレーショナルクエリを実行するときにも、使用できます。
|
|
そうするためには、リレーション名と同じ名前のメソッドを、パラメータに動的なクエリオプションを指定して、呼び出します。
|
|
例えば、下記のコードは、`status` が 1 であるユーザの記事を返します:
|
|
|
|
~~~
|
|
[php]
|
|
$user=User::model()->findByPk(1);
|
|
$posts=$user->posts(array('condition'=>'status=1'));
|
|
~~~
|
|
|
|
|
|
リレーショナルクエリのパフォーマンス
|
|
----------------------------
|
|
|
|
上述のように、イーガーローディングアプローチは、主として、多数の関連オブジェクトにアクセスする必要があるシナリオで用いられます。
|
|
これは全ての必要なテーブルを結合して、長大で複雑な SQL 文を生成します。
|
|
長大な SQL 文は、たいていの場合は、望ましいものです。
|
|
なぜなら、関連するテーブルのカラムをもとにして、フィルタリングを単純化できるからです。しかし、効率的でない場合もいくつかあります。
|
|
|
|
例として、最近の記事とそれに対するコメントを一緒に取得したい場合を考えてみて下さい。
|
|
各記事が 10 個のコメント持っていると仮定すると、単一の長大な SQL 文を使った場合は、多数の冗長な記事データが返ってくることになります。
|
|
なぜなら、すべてのコメントに対して、それの元になった記事が繰り返されるからです。
|
|
次に、別のアプローチを試してみましょう。
|
|
最初に最近の記事に対するクエリを行い、次に記事に対するクエリを行います。
|
|
この新しいアプローチでは、二つの SQL 文を実行しなければなりません。利点は、クエリの結果に冗長性が無いことです。
|
|
|
|
では、どちらのアプローチがより効率的なのでしょうか。絶対的な答えはありません。
|
|
単一の長大な SQL 文を実行する方が効率的なこともあります。何故なら、SQL 文の解釈と実行をするのに、DBMS におけるオーバーヘッドを少なく出来るからです。
|
|
その一方、単一の SQL 文を使うと、冗長なデータが増えて、その結果、データの読み出しと処理に時間がかかるようになります。
|
|
|
|
こういう理由から、Yii は必要に応じて二つのアプローチを選択できるように、`togeter` というクエリオプションを提供しています。
|
|
デフォルトでは、主たるモデルに対して `LIMIT` が適用されない限り、単一の SQL 文を生成して、イーガーローディングを実行します。
|
|
リレーションの宣言において `together` オプションを true に設定すれば、`LIMIT` が使用される場合であっても、単一の SQL 文を生成するように強制することが可能です。
|
|
そして、`together` オプションを false に設定すれば、いくつかのテーブルを別の SQL 文の中で結合するように設定することが出来ます。
|
|
例えば、最近の記事とそれに対するコメントを取得するのに、第二のアプローチを採用したい場合は、次のように `Post` クラスの `comments` リレーションを宣言します。
|
|
|
|
~~~
|
|
[php]
|
|
public function relations()
|
|
{
|
|
return array(
|
|
'comments' => array(self::HAS_MANY, 'Comment', 'post_id', 'together'=>false),
|
|
);
|
|
}
|
|
~~~
|
|
|
|
イーガーローディングを実行する場合にこのオプションを動的に設定することも可能です。
|
|
|
|
~~~
|
|
[php]
|
|
$posts = Post::model()->with(array('comments'=>array('together'=>false)))->findAll();
|
|
~~~
|
|
|
|
|
|
統計クエリ
|
|
-----------------
|
|
|
|
上述のリレーショナルなクエリの他に、Yii はいわゆる統計クエリ (または集計クエリ) もサポートします。
|
|
これは、関連するオブジェクトに関する集計的な情報、例えば各々の記事に対するコメントの数や、各々の製品の平均点数などを検索するものです。
|
|
統計クエリを実行出来るのは、`HAS_MANY` (例えば記事は多くのコメントを持つ) または `MANY_MANY` (例えば記事は多くのカテゴリーに属し、カテゴリーは多くの記事を持つ) のリレーションを持つオブジェクトに対してのみです。
|
|
|
|
統計クエリを実行することは、既に解説したリレーショナルクエリを実行することと非常に類似しています。
|
|
リレーショナルクエリで行うように、最初に統計クエリを [relations()|CActiveRecord::relations] 中で宣言する必要があります。
|
|
|
|
~~~
|
|
[php]
|
|
class Post extends CActiveRecord
|
|
{
|
|
public function relations()
|
|
{
|
|
return array(
|
|
'commentCount'=>array(self::STAT, 'Comment', 'post_id'),
|
|
'categoryCount'=>array(self::STAT, 'Category', 'post_category(post_id, category_id)'),
|
|
);
|
|
}
|
|
}
|
|
~~~
|
|
|
|
上記において、我々は二つの統計クエリを宣言しています。`commentCount` は記事に属しているコメントの数を計算します。
|
|
`categoryCount` は記事が属しているカテゴリーの数を計算します。
|
|
`Post` と `Category` の関係が `MANY_MANY` (結合テーブル `post_category` を介して)であるのに対し、`Post` と `Comment` の関係が `HAS_MANY` である点に注意してください。
|
|
このように、統計クエリの宣言は以前の節で解説したリレーション宣言と非常に類似しています。
|
|
唯一の違いはリレーションタイプが `STAT` であるということです。
|
|
|
|
上記の宣言を用いて、`$post->commentCount` という式で記事に対するコメントの数を取り出すことができます。
|
|
初めてこのプロパティにアクセスするとき、対応する結果を取り出すために暗黙のうちに SQL 文が実行されます。
|
|
すでに知っているように、これはいわゆる **レイジーローディング** アプローチです。
|
|
複数の記事についてコメント数を決定する必要があるならば、我々は **イーガーローディング** アプローチを使用することもできます。
|
|
|
|
~~~
|
|
[php]
|
|
$posts=Post::model()->with('commentCount', 'categoryCount')->findAll();
|
|
~~~
|
|
|
|
上記の文は、すべての記事に対するコメント数とカテゴリー数を取り出すために、3つの SQL 文を実行します。
|
|
レイジーローディングアプローチを使う場合、`N` 個のポストがあるならば `2*N+1` の SQL クエリを必要とします。
|
|
|
|
デフォルトでは、統計クエリは、`COUNT` 式 (従って上記の例ではコメント数とカテゴリー数) を計算します。
|
|
これは、[relations()|CActiveRecord::relations] で宣言するときに、追加のオプションを指定することでカスタマイズ可能です。
|
|
利用できるオプションは、下の通りまとめられます。
|
|
|
|
- `select`: 統計表現。デフォルトでは `COUNT(*)` であり、子オブジェクトの数を意味します。
|
|
|
|
- `defaultValue`: 統計クエリの結果を受けないレコードに割り当てられる値。
|
|
たとえば記事がコメントを持たないならば、その `commentCount` はこの値を取るでしょう。
|
|
このオプションのデフォルト値は 0 です。
|
|
|
|
- `condition`: `WHERE` 句です。デフォルト値は空です。
|
|
|
|
- `params`: 生成された SQL 文にバインドされるパラメータ値。これは "名前-値" のペアの配列として与えます。
|
|
|
|
- `order`: `ORDER BY` 句です。デフォルト値は空です。
|
|
|
|
- `group`: `GROUP BY` 句です。デフォルト値は空です。
|
|
|
|
- `having`: `HAVING` 句です。デフォルト値は空です。
|
|
|
|
|
|
名前付きスコープを使用したリレーショナルクエリ
|
|
----------------------------------
|
|
|
|
リレーショナルクエリは [名前付きスコープ](/doc/guide/database.ar#sec-12) と組み合わせて実行できます。
|
|
リレーショナルクエリは、二つの方法で利用できます。
|
|
一つ目は、名前付きスコープをメインモデルに適用させる方法、二つ目は、名前付きスコープをリレーションモデルに適用させる方法です。
|
|
|
|
下記のコードは、メインモデルに名前付きスコープを適用する方法を示します。
|
|
|
|
~~~
|
|
[php]
|
|
$posts=Post::model()->published()->recently()->with('comments')->findAll();
|
|
~~~
|
|
|
|
これは、リレーショナルでないクエリにとても似ています。
|
|
唯一の違いは、名前付きスコープのチェーンの後で `with()` をコールする点です。
|
|
このクエリは、最近公開された記事とそれらのコメントを返します。
|
|
|
|
また、下記のコードは、リレーションモデルに名前付きスコープを適用する方法を示します。
|
|
|
|
~~~
|
|
[php]
|
|
$posts=Post::model()->with('comments:recently:approved')->findAll();
|
|
// 1.1.7 以降の場合、または
|
|
$posts=Post::model()->with(array(
|
|
'comments'=>array(
|
|
'scopes'=>array('recently','approved')
|
|
),
|
|
))->findAll();
|
|
// 1.1.7 以降の場合、または
|
|
$posts=Post::model()->findAll(array(
|
|
'with'=>array(
|
|
'comments'=>array(
|
|
'scopes'=>array('recently','approved')
|
|
),
|
|
),
|
|
));
|
|
~~~
|
|
|
|
上記のクエリは、全ての記事とそれらの承認済みコメントを返します。
|
|
`comments` はリレーション名を、`recently` と `approved` は `Comment` モデルクラスで宣言された二つの名前付きスコープを示している事に注意してください。
|
|
リレーション名と名前付きスコープはコロンで区切ります。
|
|
|
|
時によっては、名前付きスコープを適用したリレーションを、上記で示されているイーガーローディングの方法ではなく、レイジーローディングの技法を使って取得する必要があるかも知れません。
|
|
その場合には、下記の文法によって目的を達することが出来ます。
|
|
|
|
~~
|
|
[php]
|
|
// リレーション名の繰り返しに注目して下さい。これは必要です。
|
|
$approvedComments = $post->comments('comments:approved');
|
|
~~
|
|
|
|
また、名前付きスコープは [CActiveRecord::relations()] で宣言されたリレーションルールの `with` オプションの中で指定することもできます。
|
|
以下の例で、`$user->posts` にアクセスすると、その記事の全ての **承認された (approved)** コメントが返されます。
|
|
|
|
~~~
|
|
[php]
|
|
class User extends CActiveRecord
|
|
{
|
|
public function relations()
|
|
{
|
|
return array(
|
|
'posts'=>array(self::HAS_MANY, 'Post', 'author_id',
|
|
'with'=>'comments:approved'),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 1.1.7 以降の場合、または
|
|
class User extends CActiveRecord
|
|
{
|
|
public function relations()
|
|
{
|
|
return array(
|
|
'posts'=>array(self::HAS_MANY, 'Post', 'author_id',
|
|
'with'=>array(
|
|
'comments'=>array(
|
|
'scopes'=>'approved'
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
~~~
|
|
|
|
> Note|注意: 1.1.7 より前では、関連したモデルに適用される名前付きスコープは、[CActiveRecord::scopes] で定義しなければなりません。結果的に、それらをパラメータ化することはできません。
|
|
|
|
バージョン 1.1.7 以降、リレーショナルクエリの名前付きスコープにパラメータを渡すことが可能になりました。
|
|
例えば、`Post` に `rated` という名前のスコープがあって、レーティングの下限を受け付ける場合、`User` から次のようにして使うことが出来ます。
|
|
|
|
~~~
|
|
[php]
|
|
$users=User::model()->findAll(array(
|
|
'with'=>array(
|
|
'posts'=>array(
|
|
'scopes'=>array(
|
|
'rated'=>5,
|
|
),
|
|
),
|
|
),
|
|
));
|
|
|
|
class Post extends CActiveRecord
|
|
{
|
|
......
|
|
|
|
public function rated($rating)
|
|
{
|
|
$this->getDbCriteria()->mergeWith(array(
|
|
'condition'=>'rating=:rating',
|
|
'params'=>array(':rating'=>$rating),
|
|
));
|
|
return $this;
|
|
}
|
|
|
|
......
|
|
}
|
|
~~~
|
|
|
|
through を使うリレーショナルクエリ
|
|
-----------------------------
|
|
|
|
`through` を使用する場合、リレーションの定義は次のようにしなければなりません。
|
|
|
|
~~~
|
|
[php]
|
|
'comments'=>array(self::HAS_MANY,'Comment',array('key1'=>'key2'),'through'=>'posts'),
|
|
~~~
|
|
|
|
上記の `array('key1'=>'key2')` において、
|
|
|
|
- `key1` は `through` で指定されているリレーション (この場合は`posts`) で定義されているキーであり、
|
|
- `key2` は リレーションが指し示すモデル (この場合は`Comment`) で定義されているキーです。
|
|
|
|
`through` は `HAS_ONE` と `HAS_MANY` の両方のリレーションで使用出来ます。
|
|
|
|
### HAS_MANY の through
|
|
|
|

|
|
|
|
`through` を使う `HAS_MANY` の一例として、ユーザがロールを通じてグループに割り当てられている場合に、特定のグループに属するユーザを取得することを挙げることが出来ます。
|
|
|
|
もう少し複雑な例としては、特定のグループに属する全てのユーザに対する全てのコメントを取得することがそれに当ります。
|
|
この場合は、単一のモデルにいくつかの `through` を使うリレーションを使用する必要があります。
|
|
|
|
~~~
|
|
[php]
|
|
class Group extends CActiveRecord
|
|
{
|
|
...
|
|
public function relations()
|
|
{
|
|
return array(
|
|
'roles'=>array(self::HAS_MANY,'Role','group_id'),
|
|
'users'=>array(self::HAS_MANY,'User',array('user_id'=>'id'),'through'=>'roles'),
|
|
'comments'=>array(self::HAS_MANY,'Comment',array('id'=>'user_id'),'through'=>'users'),
|
|
);
|
|
}
|
|
}
|
|
~~~
|
|
|
|
#### 使用例
|
|
|
|
~~~
|
|
[php]
|
|
// 一致する全ユーザとともに、全グループを取得
|
|
$groups=Group::model()->with('users')->findAll();
|
|
|
|
// 一致する全てのユーザとロールとともに、全グループを取得
|
|
$groups=Group::model()->with('roles','users')->findAll();
|
|
|
|
// グループ ID が 1 である全てのユーザとロールを取得
|
|
$group=Group::model()->findByPk(1);
|
|
$users=$group->users;
|
|
$roles=$group->roles;
|
|
|
|
// グループ ID が 1 である全てのコメントを取得
|
|
$group=Group::model()->findByPk(1);
|
|
$comments=$group->comments;
|
|
~~~
|
|
|
|
|
|
### HAS_ONE の through
|
|
|
|

|
|
|
|
`through` を使う `HAS_ONE` の使用例としては、ユーザがプロファイルを使って住所と結び付けられている場合に、ユーザの住所を取得することを挙げることが出来ます。
|
|
これらのエンティティ (ユーザ、プロファイル、住所) は全て対応するモデルを持っています。
|
|
|
|
~~~
|
|
[php]
|
|
class User extends CActiveRecord
|
|
{
|
|
...
|
|
public function relations()
|
|
{
|
|
return array(
|
|
'profile'=>array(self::HAS_ONE,'Profile','user_id'),
|
|
'address'=>array(self::HAS_ONE,'Address',array('id'=>'profile_id'),'through'=>'profile'),
|
|
);
|
|
}
|
|
}
|
|
~~~
|
|
|
|
#### 使用例
|
|
|
|
~~~
|
|
[php]
|
|
// ID が 1 であるユーザの住所を取得する
|
|
$user=User::model()->findByPk(1);
|
|
$address=$user->address;
|
|
~~~
|
|
|
|
|
|
### 自身への through
|
|
|
|
`through` は、ブリッジモデルを使って自分自身へと結び付けられるモデルにも使用することが出来ます。
|
|
以下の例では、他のユーザを指導するユーザがそれです。
|
|
|
|
|
|

|
|
|
|
|
|
この例における関係は、次のように定義することが出来ます。
|
|
|
|
~~~
|
|
[php]
|
|
class User extends CActiveRecord
|
|
{
|
|
...
|
|
public function relations()
|
|
{
|
|
return array(
|
|
'mentorships'=>array(self::HAS_MANY,'Mentorship','teacher_id','joinType'=>'INNER JOIN'),
|
|
'students'=>array(self::HAS_MANY,'User',array('student_id'=>'id'),'through'=>'mentorships','joinType'=>'INNER JOIN'),
|
|
);
|
|
}
|
|
}
|
|
~~~
|
|
|
|
#### 使用例
|
|
|
|
~~~
|
|
[php]
|
|
// ID が 1 である先生に教えられている全ての生徒を取得する
|
|
$teacher=User::model()->findByPk(1);
|
|
$students=$teacher->students;
|
|
~~~
|
|
|
|
<div class="revision">$Id$</div>
|