リレーショナルアクティブレコード

単一のデータベーステーブルからデータを選択するためにアクティブレコード (AR) を使う方法を見てきました。 この章では、AR を使って、いくつかの関係するデータベーステーブルをつなげ、結合されたデータセットを取得する方法を示します。

リレーショナル AR を使う場合は、結合すべきテーブルに主キー・外部キー制約が宣言されていることが推奨されます。 この制約がリレーショナルデータの一貫性と整合性を保持するために役立ちます。

分りやすくするために、この章では例題として、以下のエンティティ関係 (ER) 図に示されるデータベーススキーマを使用します。

ER 図

ER 図

情報: 外部キー制約のサポートは DBMS 毎に異ります。 SQLite 3.6.19 またはそれ以前のものは外部キー制約をサポートしませんが、テーブルを作成する際に制約を宣言することが出来ます。 MySQL の MyISAM エンジンは外部キーを全くサポートしません。

1. リレーションの宣言

AR のリレーショナルクエリを使用する前に、AR に対して他の AR クラスとどのように関係しているかを知らせる必要があります。

二つの AR クラスのリレーションは、AR クラスによって表現されるデータベーステーブルのリレーションと直接関係しています。 データベースの観点からは、二つのテーブル A と B の関係には、3つのタイプがあります。 1対多 (例えば tb_usertbl_post)、1対1 (例えば tbl_usertbl_profile)、多対多 (例えば tbl_categorytbl_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_TOHAS_MANY の組合せとして、MANY_MANY を説明することができます。 例えば Post は多くの Category に属しています。そして Category には多くの Post があります。

5番目に、リレーションのレコードで集計クエリを実行する特殊なタイプがあります。 STAT と呼ばれるリレーションです。 詳細については、統計クエリ を参照してください。

AR でのリレーション宣言は、CActiveRecord クラスの relations() メソッドをオーバライドすることで行います。 このメソッドはリレーション構成の配列を返します。 各々の配列要素は以下のフォーマットで示す一つのリレーションを意味します。

'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 クラスのリレーションを宣言するかを示します。

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'),
        );
    }
}

情報: 外部キーは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 の (単一または複数の) インスタンスで満たされます。 例えば、$authorUser AR インスタンスを表している場合、$author->posts を使って、関連した Post インスタンスにアクセスすることが出来ます。

2. リレーショナルクエリの実行

リレーショナルクエリを実行する最も単純な方法は、AR インスタンスのリレーショナルなプロパティを読み出すことです。 プロパティが以前にアクセスされていない場合には、リレーショナルクエリが開始されます。 このクエリは二つの関係するテーブルを結合し、現行の AR インスタンスのプライマリキーでフィルタリングするものです。 そして、クエリの結果は、関連する AR の (単一または複数の) インスタンスとして、プロパティに保存されます。 これは レイジーローディング (Lazy Loading) アプローチとして知られており、リレーショナルクエリは関連するオブジェクトが最初にアクセスされて初めて実行されます。 以下の例は実際にこのアプローチをどのように使用するかを示します。

// ID が 10 である記事を取得
$post=Post::model()->findByPk(10);
// 記事の著者を取得。リレーショナルクエリはここで実行される
$author=$post->author;

情報: リレーションにより関連したインスタンスが取得できない場合、対応するプロパティは null または空の配列となります。 BELONGS_TOHAS_ONE リレーションの場合結果は null です。 HAS_MANYMANY_MANY では空の配列です。 HAS_MANYMANY_MANY リレーションは、オブジェクトの配列を返すため、個々のプロパティにアクセスする前に、結果の配列をループする必要があることに注意してください。 そうでなければ、"Trying to get property of non-object (非オブジェクトのプロパティを取得しようとしている)" というエラーが発生します。

レイジーローディングアプローチは使うのに非常に便利ですが、それはいくつかの場合に効率的ではありません。 例えば N 個の著者情報にアクセスする場合、レイジーローディングアプローチを使うと N 個のジョインクエリを発行しなければなりません。 この状況ではいわゆる イーガーローディング (Eager Loading) アプローチをとる必要があります。

イーガーローディングアプローチでは、主となる AR インスタンスと共に関連する AR インスタンスを取得します。 これは、AR において findfindAll のいずれかと共に with() メソッドを用いることで達成されます。 例えば、

$posts=Post::model()->with('author')->findAll();

上記のコードは Post インスタンスの配列を返します。 レイジーアプローチとは異なり、author プロパティにアクセスする前に、各々の Post インスタンスの author プロパティは関連した User インスタンスを格納しています。 記事ごとにジョインクエリを実行する代わりに、イーガーローディングアプローチでは、一回のジョインクエリによって、すべての記事を著者と共に取得します。

複数のリレーション名を with() メソッド中で指定することができ、イーガーローディングアプローチでは一度で全ての情報を取得できます。 例えば、以下のコードは、著者とカテゴリーを付加して、すべての記事を取得します。

$posts=Post::model()->with('author','categories')->findAll();

イーガーローディングを入れ子で実行することもできます。 リレーション名のリストの代わりに、以下のようにリレーション名の階層的な表現を with() メソッドに渡します。

$posts=Post::model()->with(
    'author.profile',
    'author.posts',
    'categories')->findAll();

上記の例は、著者とカテゴリーと共にすべての記事を取得します。 さらに各々の著者のプロフィールと記事を取得します。

イーガーローディングは、下記のように、CDbCriteria::with プロパティを指定しても実行することが出来ます。

$criteria=new CDbCriteria;
$criteria->with=array(
    'author.profile',
    'author.posts',
    'categories',
);
$posts=Post::model()->findAll($criteria);

または

$posts=Post::model()->findAll(array(
    'with'=>array(
        'author.profile',
        'author.posts',
        'categories',
    )
));

3. 関連するモデルを取得しないリレーショナルクエリを実行する

場合によっては、リレーショナルクエリを実行する必要があるけれども、関連するモデルは取得したくない、ということがあります。 たとえば、数多くの Post を投稿した User が沢山いるとしましょう。 記事は公開されている場合もあれば、下書き状態にとどまっている場合もあります。 これは Post モデルの published フィールドによって決定されます。 そして、公開されている記事を持っているユーザをすべて取得したいけれども、記事そのものには関心がない、という場合です。 これは以下のようにして達成することが出来ます。

$users=User::model()->with(array(
    'posts'=>array(
        // 記事は SELECT したくない
        'select'=>false,
        // けれども、公開されている記事を持つユーザだけを取得したい
        'joinType'=>'INNER JOIN',
        'condition'=>'posts.published=1',
    ),
))->findAll();

4. リレーショナルクエリのオプション

既に述べたように、リレーションの宣言において追加のオプションを指定することが出来ます。 これらのオプションは、"名前-値" のペアとして指定され、リレーショナルクエリをカスタマイズするのに用いられます。 それらの概要は以下の通りです。

  • 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 以降で利用可能です。

  • joinOptions: USE INDEX のような JOIN 後の操作を設定するためのプロパティです。 文字列型の値を HAS_MANYMANY_MANY のリレーションの JOIN のために使うことが出来ます。 一方、配列型の値は MANY_MANY リレーションのためだけに使われることを想定しています。 配列の最初の要素がジャンクションテーブルの JOIN のために使われ、2番目の要素がターゲットテーブルの JOIN のために使われます。 このオプションは、バージョン 1.1.15 以降で利用可能です。

  • 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: 関連するデータを取得する際に、ブリッジとして使用されるモデルのリレーションの名前。 このオプションは、バージョン 1.1.7 以降で、HAS_MANY および MANY_MANY のリレーションに対して 使用可能です。1.1.14 以降は、BELONGS_TO に対しても使用出来ます。

以下では、User における posts リレーション宣言を修正して、上記のオプションのいくつかを含むようにしてみましょう。

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 としてアクセスすると、著者の記事を作成日時の降順でソートして取得することが出来ます。 また、各記事のインスタンスには、そのカテゴリも付加されます。

注意: イーガーローディングを使う場合、'order'、'group'、'having'、'limit' および 'offset' というようなオプションは無視されます。このようなパラメータを適用したいときは、 メインモデルのクライテリアのレベルで設定しなければなりません。

5. カラム名の曖昧さを無くする

結合される二つ以上のテーブルに同じ名前のカラムが出現する場合、カラム名の曖昧さを無くする必要があります。 これは、カラム名の前にテーブルのエイリアス(別名)を追加することで行います。

リレーショナル AR クエリにおいては、主テーブルのエイリアスは t に固定されます。 一方、関連するテーブルのエイリアスは、デフォルトでは、対応するリレーション名と同じものになります。 例えば、次の文では、Post および Comment のエイリアスは、それぞれ、t および comments です。

$posts=Post::model()->with('comments')->findAll();

今、仮に、PostComment も、それぞれ、記事またはコメントの作成日時を示す create_time というカラムを持っているとします。 そして、記事とそれに対するコメントを一緒にして取得するときに、先ず記事の作成日時でソートし、次にコメントの作成日時でソートしたいとします。 このとき、create_time というカラム名の曖昧さを無くすために次のようにします。

$posts=Post::model()->with('comments')->findAll(array(
    'order'=>'t.create_time, comments.create_time'
));

ヒント: 関連するテーブルのデフォルトのエイリアスはリレーション名です。リレーションを別のリレーションの中から参照する 場合でも、そのテーブルのエイリアスは前者のリレーション名であり、親のリレーション名を前置したものにはならないということに 注意して下さい。例えば、'author.group' というリレーションのテーブルエイリアスは 'group' であり、'author.group' ではありません。

$posts=Post::model()->with('author', 'author.group')->findAll(array(
  'order'=>'group.name, author.name, t.title'
));

テーブルエイリアスの衝突を回避するために、リレーションの alias プロパティを設定することが出来ます。

$comments=Comment::model()->with(array(
  'author',
  'post',
  'post.author'=>array('alias'=>'p_author')))->findAll(array(
  'order'=>'author.name, p_author.name, post.title'
));

6. 動的リレーショナルクエリオプション

with()with オプションの両方の場合とも、動的なリレーショナルクエリのオプションを使用することができます。 動的なオプションは、relations() メソッドの中で指定された既存のオプションを上書きします。 たとえば、上記の User モデルにおいて、イーガーローディングのアプローチを使って、ある著者に属する記事を 昇順 (リレーション定義中の order オプションは降順です) で取得したいならば、以下のように行います。

User::model()->with(array(
    'posts'=>array('order'=>'posts.create_time ASC'),
    'profile',
))->findAll();

動的なクエリオプションは、レイジーローディングのアプローチを使ってリレーショナルクエリを実行するときにも、使用できます。 そうするためには、リレーション名と同じ名前のメソッドを、パラメータに動的なクエリオプションを指定して、呼び出します。 例えば、下記のコードは、status が 1 であるユーザの記事を返します:

$user=User::model()->findByPk(1);
$posts=$user->posts(array('condition'=>'status=1'));

7. リレーショナルクエリのパフォーマンス

上述のように、イーガーローディングアプローチは、主として、多数の関連オブジェクトにアクセスする必要があるシナリオで用いられます。 これは全ての必要なテーブルを結合して、長大で複雑な SQL 文を生成します。 長大な SQL 文は、たいていの場合は、望ましいものです。 なぜなら、関連するテーブルのカラムをもとにして、フィルタリングを単純化できるからです。しかし、効率的でない場合もいくつかあります。

例として、最近の記事とそれに対するコメントを一緒に取得したい場合を考えてみて下さい。 各記事が 10 個のコメント持っていると仮定すると、単一の長大な SQL 文を使った場合は、多数の冗長な記事データが返ってくることになります。 なぜなら、すべてのコメントに対して、それの元になった記事が繰り返されるからです。 次に、別のアプローチを試してみましょう。 最初に最近の記事に対するクエリを行い、次に記事に対するクエリを行います。 この新しいアプローチでは、二つの SQL 文を実行しなければなりません。利点は、クエリの結果に冗長性が無いことです。

では、どちらのアプローチがより効率的なのでしょうか。絶対的な答えはありません。 単一の長大な SQL 文を実行する方が効率的なこともあります。何故なら、SQL 文の解釈と実行をするのに、DBMS におけるオーバーヘッドを少なく出来るからです。 その一方、単一の SQL 文を使うと、冗長なデータが増えて、その結果、データの読み出しと処理に時間がかかるようになります。

こういう理由から、Yii は必要に応じて二つのアプローチを選択できるように、together というクエリオプションを提供しています。 デフォルトでは、主たるモデルに対して LIMIT が適用されない限り、単一の SQL 文を生成して、イーガーローディングを実行します。 リレーションの宣言において together オプションを true に設定すれば、LIMIT が使用される場合であっても、単一の SQL 文を生成するように強制することが可能です。 そして、together オプションを false に設定すれば、いくつかのテーブルを別の SQL 文の中で結合するように設定することが出来ます。 例えば、最近の記事とそれに対するコメントを取得するのに、第二のアプローチを採用したい場合は、次のように Post クラスの comments リレーションを宣言します。

public function relations()
{
    return array(
        'comments' => array(self::HAS_MANY, 'Comment', 'post_id', 'together'=>false),
    );
}

イーガーローディングを実行する場合にこのオプションを動的に設定することも可能です。

$posts = Post::model()->with(array('comments'=>array('together'=>false)))->findAll();

8. 統計クエリ

上述のリレーショナルなクエリの他に、Yii はいわゆる統計クエリ (または集計クエリ) もサポートします。 これは、関連するオブジェクトに関する集計的な情報、例えば各々の記事に対するコメントの数や、各々の製品の平均点数などを検索するものです。 統計クエリを実行出来るのは、HAS_MANY (例えば記事は多くのコメントを持つ) または MANY_MANY (例えば記事は多くのカテゴリーに属し、カテゴリーは多くの記事を持つ) のリレーションを持つオブジェクトに対してのみです。

統計クエリを実行することは、既に解説したリレーショナルクエリを実行することと非常に類似しています。 リレーショナルクエリで行うように、最初に統計クエリを relations() 中で宣言する必要があります。

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 は記事が属しているカテゴリーの数を計算します。 PostCategory の関係が MANY_MANY (結合テーブル post_category を介して)であるのに対し、PostComment の関係が HAS_MANY である点に注意してください。 このように、統計クエリの宣言は以前の節で解説したリレーション宣言と非常に類似しています。 唯一の違いはリレーションタイプが STAT であるということです。

上記の宣言を用いて、$post->commentCount という式で記事に対するコメントの数を取り出すことができます。 初めてこのプロパティにアクセスするとき、対応する結果を取り出すために暗黙のうちに SQL 文が実行されます。 すでに知っているように、これはいわゆる レイジーローディング アプローチです。 複数の記事についてコメント数を決定する必要があるならば、我々は イーガーローディング アプローチを使用することもできます。

$posts=Post::model()->with('commentCount', 'categoryCount')->findAll();

上記の文は、すべての記事に対するコメント数とカテゴリー数を取り出すために、3つの SQL 文を実行します。 レイジーローディングアプローチを使う場合、N 個のポストがあるならば 2*N+1 の SQL クエリを必要とします。

デフォルトでは、統計クエリは、COUNT 式 (従って上記の例ではコメント数とカテゴリー数) を計算します。 これは、relations() で宣言するときに、追加のオプションを指定することでカスタマイズ可能です。 利用できるオプションは、下の通りまとめられます。

  • select: 統計表現。デフォルトでは COUNT(*) であり、子オブジェクトの数を意味します。

  • defaultValue: 統計クエリの結果を受けないレコードに割り当てられる値。 たとえば記事がコメントを持たないならば、その commentCount はこの値を取るでしょう。 このオプションのデフォルト値は 0 です。

  • condition: WHERE 句です。デフォルト値は空です。

  • params: 生成された SQL 文にバインドされるパラメータ値。これは "名前-値" のペアの配列として与えます。

  • order: ORDER BY 句です。デフォルト値は空です。

  • group: GROUP BY 句です。デフォルト値は空です。

  • having: HAVING 句です。デフォルト値は空です。

9. 名前付きスコープを使用したリレーショナルクエリ

リレーショナルクエリは 名前付きスコープ と組み合わせて実行できます。 リレーショナルクエリは、二つの方法で利用できます。 一つ目は、名前付きスコープをメインモデルに適用させる方法、二つ目は、名前付きスコープをリレーションモデルに適用させる方法です。

下記のコードは、メインモデルに名前付きスコープを適用する方法を示します。

$posts=Post::model()->published()->recently()->with('comments')->findAll();

これは、リレーショナルでないクエリにとても似ています。 唯一の違いは、名前付きスコープのチェーンの後で with() をコールする点です。 このクエリは、最近公開された記事とそれらのコメントを返します。

また、下記のコードは、リレーションモデルに名前付きスコープを適用する方法を示します。

$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 はリレーション名を、recentlyapprovedComment モデルクラスで宣言された二つの名前付きスコープを示している事に注意してください。 リレーション名と名前付きスコープはコロンで区切ります。

時によっては、名前付きスコープを適用したリレーションを、上記で示されているイーガーローディングの方法ではなく、レイジーローディングの技法を使って取得する必要があるかも知れません。 その場合には、下記の文法によって目的を達することが出来ます。

~~ [php] // リレーション名の繰り返しに注目して下さい。これは必要です。 $approvedComments = $post->comments('comments:approved'); ~~

また、名前付きスコープは CActiveRecord::relations() で宣言されたリレーションルールの with オプションの中で指定することもできます。 以下の例で、$user->posts にアクセスすると、その記事の全ての 承認された (approved) コメントが返されます。

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'
                    ),
                ),
            ),
        );
    }
}

注意: 1.1.7 より前では、関連したモデルに適用される名前付きスコープは、CActiveRecord::scopes で定義しなければなりません。結果的に、それらをパラメータ化することはできません。

バージョン 1.1.7 以降、リレーショナルクエリの名前付きスコープにパラメータを渡すことが可能になりました。 例えば、Postrated という名前のスコープがあって、レーティングの下限を受け付ける場合、User から次のようにして使うことが出来ます。

$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;
    }
 
    ......
}

10. through を使うリレーショナルクエリ

through を使用する場合、リレーションの定義は次のようにしなければなりません。

'comments'=>array(self::HAS_MANY,'Comment',array('key1'=>'key2'),'through'=>'posts'),

上記の array('key1'=>'key2') において、

  • key1through で指定されているリレーション (この場合はposts) で定義されているキーであり、
  • key2 は リレーションが指し示すモデル (この場合はComment) で定義されているキーです。

throughHAS_ONE, BELONGS_TO および HAS_MANY のリレーションで使用出来ます。

HAS_MANY の through

HAS_MANY through ER

HAS_MANY through ER

through を使う HAS_MANY の一例として、ユーザがロールを通じてグループに割り当てられている場合に、特定のグループに属するユーザを取得することを挙げることが出来ます。

もう少し複雑な例としては、特定のグループに属する全てのユーザに対する全てのコメントを取得することがそれに当ります。 この場合は、単一のモデルにいくつかの through を使うリレーションを使用する必要があります。

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'),
        );
    }
}

使用例

// 一致する全ユーザとともに、全グループを取得
$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

HAS_ONE through ER

HAS_ONE through ER

through を使う HAS_ONE の使用例としては、ユーザがプロファイルを使って住所と結び付けられている場合に、ユーザの住所を取得することを挙げることが出来ます。 これらのエンティティ (ユーザ、プロファイル、住所) は全て対応するモデルを持っています。

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'),
        );
    }
}

使用例

// ID が 1 であるユーザの住所を取得する
$user=User::model()->findByPk(1);
$address=$user->address;

自身への through

through は、ブリッジモデルを使って自分自身へと結び付けられるモデルにも使用することが出来ます。 以下の例では、他のユーザを指導するユーザがそれです。

through self ER

through self ER

この例における関係は、次のように定義することが出来ます。

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'),
        );
    }
}

使用例

// ID が 1 である先生に教えられている全ての生徒を取得する
$teacher=User::model()->findByPk(1);
$students=$teacher->students;

11. JOIN 後の操作

バージョン 1.1.15 以降は、JOIN 後の操作を追加して設定することが出来ます。 CBaseActiveRelation::$joinOptions が追加されました。 下記のようなモデルとリレーションがあるとします。

class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts' => array(self::HAS_MANY, 'Post', 'user_id'),
        );
    }
}
 
class Post extends CActiveRecord
{
    public function relations()
    {
        return array(
            'user' => array(self::BELONGS_TO, 'User', 'user_id'),
            'tags' => array(self::MANY_MANY, 'Tag', '{{post_tag}}(post_id, tag_id)'),
        );
    }
}
 
class Tag extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts' => array(self::MANY_MANY, 'Post', '{{post_tag}}(tag_id, post_id)'),
        );
    }
}

USE INDEX 句を使ったクエリコードのサンプルを示します。

$users=User::model()->findAll(array(
    'select'=>'t.id,t.name',
    'with'=>array(
        'posts'=>array(
            'alias'=>'p',
            'select'=>'p.id,p.title',
            'joinOptions'=>'USE INDEX(post__user)',
        ),
    ),
));
 
$posts=Post::model()->findAll(array(
    'select'=>'t.id,t.title',
    'with'=>array(
        'tags'=>array(
            'alias'=>'a',
            'select'=>'a.id,a.name',
            'joinOptions'=>'USE INDEX(post_tag__tag) USE INDEX(post_tag__post)',
        ),
    ),
));
 
$posts=Post::model()->findAll(array(
    'select'=>'t.id,t.title',
    'with'=>array(
        'tags'=>array(
            'alias'=>'a',
            'select'=>'a.id,a.name',
            'joinOptions'=>array(
                'USE INDEX(post_tag__tag) USE INDEX(post_tag__post)',
                'USE INDEX(tag__name)',
            ),
        ),
    ),
));

上記のコードは、それぞれ以下のような MySQL クエリを生成します。

SELECT
    `t`.`id` AS `t0_c0`, `t`.`name` AS `t0_c1`,
    `p`.`id` AS `t1_c0`, `p`.`title` AS `t1_c2`
FROM `tbl_user` `t`
LEFT OUTER JOIN `tbl_post` `p`
    USE INDEX(post__user) ON (`p`.`user_id`=`t`.`id`);
 
SELECT
    `t`.`id` AS `t0_c0`, `t`.`title` AS `t0_c2`,
    `a`.`id` AS `t1_c0`, `a`.`name` AS `t1_c1`
FROM `tbl_post` `t`
LEFT OUTER JOIN `tbl_post_tag` `tags_a`
    USE INDEX(post_tag__tag) USE INDEX(post_tag__post) ON (`t`.`id`=`tags_a`.`post_id`)
LEFT OUTER JOIN `tbl_tag` `a` ON (`a`.`id`=`tags_a`.`tag_id`);
 
SELECT
    `t`.`id` AS `t0_c0`, `t`.`title` AS `t0_c2`,
    `a`.`id` AS `t1_c0`, `a`.`name` AS `t1_c1`
FROM `tbl_post` `t`
LEFT OUTER JOIN `tbl_post_tag` `tags_a`
    USE INDEX(post_tag__tag) USE INDEX(post_tag__post) ON (`t`.`id`=`tags_a`.`post_id`)
LEFT OUTER JOIN `tbl_tag` `a`
    USE INDEX(tag__name) ON (`a`.`id`=`tags_a`.`tag_id`);

クエリオプション $joinOptions は、次に示すように、リレーションの宣言の中で設定することも出来ます。

class Post extends CActiveRecord
{
    public function relations()
    {
        return array(
            'user' => array(self::BELONGS_TO, 'User', 'user_id'),
            'tags' => array(self::MANY_MANY, 'Tag', '{{post_tag}}(post_id, tag_id)',
                'joinOptions' => array(
                    'USE INDEX(post_tag__tag) USE INDEX(post_tag__post)',
                    'USE INDEX(tag__name)',
                ),
            ),
        );
    }
}

Be the first person to leave a comment

Please to leave your comment.