0 follower

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

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

リレーショナルARを使うためには、主外部キー関係が結合すべきテーブル間で正しく定義されている必要があります。 ARはこれらのリレーションに関してどのようにテーブルを結合するかを決定するために、メタデータに依ります。

注意: 1.0.1版からはデータベースに外部キー制約が定義されていない場合でもリレーショナルARを使用することができます。

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

ER Diagram ER図

ER Diagram ER図

情報: 外部キー制約のサポートはDBMS毎に異ります。

SQLiteは外部キーをサポートしませんが、テーブルを作成する際に制約を宣言します。 ARはリレーショナルなクエリを正しくサポートするためにこの制約宣言を利用します。

MySQLはInnoDBエンジンの場合には外部キーをサポートしますが、MyISAMの場合はしません。 従って、MySQLデータベースの場合にはInnoDBの使用を推奨します。 MyISAMの場合には、以下のようなトリックを使用してリレーショナルなクエリを実行することができます。

CREATE TABLE Foo
(
  id INTEGER NOT NULL PRIMARY KEY
);
CREATE TABLE bar
(
  id INTEGER NOT NULL PRIMARY KEY,
  fooID INTEGER
     COMMENT 'CONSTRAINT FOREIGN KEY (fooID) REFERENCES Foo(id)'
);

上記では説明したリレーションを認識させるため、ARから読めるように外部キー制約を記述するCOMMENTキーワードを使用します。

1. リレーションの宣言

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

2つのARクラスのリレーションは、ARクラスによって表現されるデータベーステーブルのリレーションと直接関係しています。 データベースの観点からは、2つのテーブルAとBの関係には、3つのタイプがあります。 1対多(例えばUserPost)、1対1(例えばUserProfile)、多対多(例えばCategoryPost)。 ARでは、以下の4種類のリレーションがあります。

  • BELONGS_TO: テーブルAとBの関係が1対多ならば、BはAに属しています(e.g. PostUserに属す)。

  • HAS_MANY: 同じくテーブルAとBの関係が1対多ならば、Aは多くのBを持っています(e.g. Userは多くのPostを持つ)。

  • HAS_ONE: これはAがたかだか1つのBを持っているHAS_MANYの特例です(e.g. Userはたかだか1つのProfileを持つ)。

  • MANY_MANY: これはデータベースにおいて多対多の関係と対応します。 多対多の関係を1対多の関係に分割するために、関連したテーブルが必要です。なぜなら 大部分のDBMSは、直接多対多の関係をサポートしないためです。 例題のデータベーススキーマでは、PostCategoryはこの目的のために使用されます。 AR用語では、BELONGS_TOHAS_MANYの組合せとして、MANY_MANYを説明することができます。 例えばPostは多くのCategoryに属しています。そしてCategoryには多くのPostがあります。

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

'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', ...付加オプション)

ここでVarNameはリレーションの名前です。RelationTypeはリレーションのタイプを指定します。 そしてそれは4つの定数、self::BELONGS_TOself::HAS_ONEself::HAS_MANYself::MANY_MANYのうちの1つです。 ClassNameはこのARクラスに関連したARクラスの名前です。 ForeignKeyはリレーションに関係する外部キーを指定します。 付加オプションは各々のリレーション(後述)の終わりに指定すことができます。

以下のコードでどのようにUserPostクラスのリレーションを宣言するかを示します。

class Post extends CActiveRecord
{
    public function relations()
    {
        return array(
            'author'=>array(self::BELONGS_TO, 'User', 'authorID'),
            'categories'=>array(self::MANY_MANY, 'Category', 'PostCategory(postID, categoryID)'),
        );
    }
}
 
class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'authorID'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'ownerID'),
        );
    }
}

情報: 外部キーは2個以上の列で構成される複合キーでもかまいません。 この場合は名前は外部キー名の結合となるべきであり、スペースまたはカンマで分割されます。 MANY_MANYのリレーションにおいては、関連したテーブル名は、外部キーでも指定されなければなりません。 例えば、Postにおけるcategoriesリレーションは外部キーPostCategory(postID, categoryID)により指定されます。

ARクラスのリレーションの宣言は、各々のリレーションのために暗黙のうちにプロパティをクラスに加えます。 リレーショナルなクエリが実行された後、対応するプロパティは関連するARインスタンスで満されます。 例えば、$authorUserARインスタンスを表している場合、関連したPostインスタンスにアクセスするために$author->postsを 使うことがあります。

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

リレーショナルクエリを実行する最も単純な方法は、ARインスタンスのリレーショナルなプロパティを読み出すことです。 プロパティが以前にアクセスされていない場合には、リレーショナルクエリは初期化されます。 そのクエリは2つの関係するテーブルを結合し、現行のARインスタンスの主キーでフィルタリングされます。 これはレイジーローディングアプローチとして知られており、リレーショナルなクエリは関連するオブジェクトが最初にアクセスされるときに実行されます。 以下の例は実際にこのアプローチをどのように使用するかを示します。

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

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

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

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

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

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

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

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

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

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

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

注意: with()の使用法はバージョン1.0.2から変わりました。 対応するAPIドキュメンテーションを注意深く読んでください。

YiiのAR実装は非常に効率的です。 N個のHAS_MANYまたはMANY_MANYリレーションを含んでいるオブジェクトの階層をイーガーローディングする時、 必要とする結果を得るためにN+1個のSQLクエリを必要とします。 従って、先程の例ではpostscategoriesのプロパティを取得するため、3つのSQLクエリを実行する必要があることを意味します。 他のフレームワークは、1つのSQLクエリだけを用いたより急進的なアプローチをとっています。 ぱっと見には急進的なアプローチはより効率的にみえます。というのはより少ないクエリがDBMSによって解析され実行されるのためです。 そのアプローチは実際には2つの理由から非実用的です。 第1に結果の中に多くの反復的なデータコラムがあり、それを送信し処理する余分な時間がかかるためです。 第2に結果セットの列の数は関係するテーブルの数で指数的に増大します。 そして、リレーションがより複雑になるにつれ、管理不可能になります。

バージョン1.0.2から、1つのSQLクエリだけでリレーショナルなクエリを実施することもできます。 単に、together()呼び出しをwith()の後に追加するだけです。 例えば、

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

上記のクエリは1つのSQLクエリで実行されます。 togetherメソッドを呼出さなければ、PostUserProfileテーブルをジョインするクエリと、 UserPostテーブルをジョインするクエリの2つのSQLクエリを必要とします。

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

さらなるオプションがリレーション宣言において指定されることができることを述べました。 これらのオプションは、名前-値のペアとして指定されますが、リレーションの質問をカスタマイズするのに用いられます。 それらは以下の通りまとめられます。

  • select: リレーションのあるARクラスのために選ばれるコラムのリスト。 デフォルトは'*'でありすべてのコラムを意味します。 コラム名が式(例えば、COUNT(??.name)AS nameCountのような)に現れるならば、aliasTokenを使って曖昧さをなくされなければなりません。

  • condition: WHERE句です。デフォルトは空で無条件を意味します。 注意:コラム参照はaliasTokenを用いてて曖昧さをなくされなければなりません(例えば??.id=10)。

  • params: 生成されたSQL文に縛らるパラメータ。 これは名前-値のペアの配列として与えられなければなりません。 このオプションはバージョン1.0.3から利用できるようになりました。

  • on: ON句です。ここで指定される条件は、ANDオペレーターを使用しJOIN条件に追加されます。 注意: コラム参照はaliasTokenを使って曖昧さをなくす必要があることに注意すべきです(例えば??.id=10)。 このオプションはバージョン1.0.2から利用できるようになりました。

  • order: ORDER BY句です。デフォルトでは空で無条件を意味します。 注意:コラム参照はaliasTokenを用いてて曖昧さをなくされなければなりません(例えば??.age DESC)。

  • with: 子供のリレーションを持つオブジェクトのリストであり、このオブジェクトと共にロードすべきオブジェクトです。 このオプションを不適切に使用すると、無限リレーションループが形成される可能性がありますので、注意してください。

  • joinType: このリレーションのジョインタイプで、デフォルトではLEFT OUTER JOINです。

  • aliasToken: コラム接頭辞の場所取りです。コラム参照の曖昧さをなくすために対応するテーブル別名と置き換えられます。 デフォルトでは'??.'です。

  • alias: このリレーションと結びついたテーブルの別名です。 このオプションはバージョン1.0.1から利用できるようになりました。 デフォルトはナルであり、テーブル別名が自動的に生成されることを意味します。 これは以下の点からaliasTokenと異なります。aliasTokenは単なる場所取りであり、実際のテーブル別名と置き換えられるためです。

  • together: テーブルがこのリレーションと結びついたかどうかは、主テーブルと共にジョインとして強制されなければなりません。 このオプションはHAS_MANYとMANY_MANY関係のときのみ意味があります。 このオプションがセットされないか偽である場合、HAS_MANYまたはMANY_MANYリレーションはパフォーマンスを向上させるためにのJOIN文を持ちます。 このオプションはバージョン1.0.3から利用できるようになりました。

  • group: GROUP BY句です。デフォルトは空です。 注意:コラム参照はaliasTokenを用いてて曖昧さをなくされなければなりません(例えば??.age)。

  • having: HAVING句です。デフォルトは空です。 注意:コラム参照はaliasTokenを用いてて曖昧さをなくされなければなりません(例えば??.age)。 このオプションはバージョン1.0.1から利用できるようになりました。

さらに、以下のオプションは、レイジーローディングの間、特定の関係のために利用できます:

  • limit: 選択される列の制限。このオプションはBELONGS_TOリレーションには適用されません。

  • offset: 選択される列のオフセット。このオプションはBELONGS_TOリレーションには適用されません。

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

class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'authorID',
                            'order'=>'??.createTime DESC',
                            'with'=>'categories'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'ownerID'),
        );
    }
}

さて今$author->postsのようにアクセスする場合、下降順に生成時間によってソートされる著者のポストを得るでしょう。 それぞれのポストインスタンスも、その塔載されたカテゴリーを持ちます。

情報: 列名が結び付けられている2つ以上のテーブルに現れるとき、曖昧さが無い必要があります。 テーブル名で列名に接頭辞を付けることによってこれは成されます。 たとえばidTeam.idとなります。 しかしながらARリレーションのクエリにおいては、SQL文はシステム的に各々のテーブルに別名を与えるARによって自動的に発生するので、 この自由がありません。 したがって、列名の衝突を避けるために、曖昧さを無くす必要がある列の存在を示すために、場所取りを使います。 ARは場所取りを適当なテーブル別名と入れ替えて列の曖昧さをなくします。

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

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

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

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

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

5. 統計クエリ

注意: 統計クエリはバージョン1.0.4からサポートされます。

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

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

class Post extends CActiveRecord
{
       public function relations()
       {
              return array(
                     'commentCount'=>array(self::STAT, 'Comment', 'postID'),
                     'categoryCount'=>array(self::STAT, 'Category', 'PostCategory(postID, categoryID)'),
              );
       }
}

上記において、我々は2つの統計クエリを宣言します。commentCountはポストに属しているコメントの数を計算します。 categoryCountはポストが属しているカテゴリーの数を計算します。 PostCategoryの関係がMANY_MANY(ジョインテーブルPostCategoryを介して)であるのに対し、 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句です。デフォルト値は空です。

6. Named Scope を使用したリレーショナルクエリ

注意: Named Scope はバージョン 1.0.5 以降で有効です。

リレーショナルクエリは Named Scope と組み合わせて実行できます。 リレーショナルクエリは、2 つの方法で利用できます。 1つ目は、Named Scope をメインモデルに適用させる方法、2つ目は、Named Scope をリレーションモデルに適用させる方法です。

下記のコードは、メインモデルに Named Scope を適用する方法を示します。

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

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

また、下記のコードは、リレーションモデルに Named Scope を適用する方法を示します。

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

上記クエリは、全ての投稿とそれらの承認済みコメントを返します。 comments はリレーション名を、recentlyapprovedComment モデルクラスで宣言された 2 つの Named Scope を示している事に注意してください。 リレーション名と Named Scope はコロン区切りで指定します。

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

class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'authorID',
                'with'=>'comments:approved'),
        );
    }
}

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