CakePHPのアソシエーション

posted 2009-11-26 | written by mon_sat

CakePHPには、非常に強力なアソシエーションという機能があります。
これは相互に関連する複数のModel(テーブル)を一括して扱うための優れた仕組みです。

便利なアソシエーションですが、マニュアルを読んで理解したつもりでも、実際に活用する段になって、どのように設定すればよいのか迷うことがあります。

CakePHPに不慣れな方がアソシエーションで迷っていたときに、どのように教えてあげると分かりやすいのかという視点で、アソシエーションについてまとめてみました。

ここではHABTM(Has And BelongsTo Many)については触れません。それはまた別の機会で。

よくある勘違い

一般にAとBというふたつのモデルがあったとき、AモデルがBモデルを『ひとつ持っているときはhasOne』で『複数持っているときはhasMany』と認識している場合がありますが、その理解のままだと、ときに混乱してしまいます。

よくあるのが、以下のようなケースです。

  • ブログの記事を扱ったArticleモデルと、その記事の『編集中・投稿済・削除』等のステータスを扱うStatusモデルがあり
  • ArticleモデルとStatusモデルのアソシエーションを検討したとき
  • Article hasOne Status と間違ってしまう

ということがあります。

記事が、ひとつのステータスを保持するわけですから、hasOneだろうと考えると、『あれ?どうやってこれ設定するんだ???』となってしまいがちです。

つまり、上記の『ひとつ持っている』『複数持っている』という考え方は、忘れたほうが良いのです。

アソシエーションの基本

それでは、まず、基本的なところからおさらいしつつ、なぜ上述のような混乱に陥るのかを説明していきたいと思います。

アソシエーションの基本原則其の一:外部キーを持っているときはbelongsTo

まず、belongsToからおさらいしましょう。
belongsToは、あるモデル(B)があるモデル(A)に属している時に、B belongsTo A とアソシエーションを設定します。

『属してるってなんぞや?』というと、簡単に言えば、『hasOneもしくはhasManyされている』ということです。
A hasOne B もしくは、A hasMany Bという関係が成り立っている場合、B belongsTo Aというアソシエーションとなります。

hasOne や hasMany が存在せず、belongsTo だけ設定した場合でも、アプリによっては何の支障もなく動作します。
しかし、ここでは基礎を学ぶという意味で、逆方向のアソシエーションの存在と『対』で理解してもらうことから始めます。

外部キー

相手方のモデルの主キー(id)を『外部キー』といい、belongsToが設定されたモデルには、必ずこのフィールドが存在します。

たとえば、上述のArticleモデルが複数のコメントを扱うCommentモデルとhasManyの関係にある場合、

  • Article hasMany Comment
  • Comment belongsTo Article

ですから、Commentモデルには、article_idというフィールドが必要になります。

逆に言えば、以下の原則が成り立つということです。

アソシエーションの基本原則其の二:hasOneおよびhasManyの相手方のモデルには外部キーが存在する

A hasOne B もしくは A hasMany B というアソシエーションの場合、AモデルにはBモデルの外部キーはありません。
分かりやすいhasManyの例で説明すると、上述の例で、Articleモデルにcomment_idというフィールドを設定することは不可能です。
だってコメントは複数存在できるので、フィールドがひとつでは足りませんから(笑)

hasOne も hasMany と同類

『じゃあ、hasOneならあり得る?』かというと、そういうものではありません。
hasOneの場合、どっちに外部キーを置けばよいか分かりづらいため、外部キーを頼りに考えていくと、より混乱をきたしやすいようです。

そういう場合は、心の中で『hasOne は、hasMany の特殊な一形態』と唱えるようにしましょう。

だって、相手方(関連モデル)が『複数くっついているか、ひとつしかくっつかないか』の差でしか無いわけですから、hasOneなら自モデルに外部キーとなるはずはありません。

もちろんhasOneとhasManyはもっと本質的に違いますが、ここではどのようにアソシエーションすればよいかを考えるために単純化しています

外部キーの位置関係のまとめ

外部キーは、hasOneおよびhasManyのときには『相手方のモデルに存在』し、belongsToしているときは『自モデルに存在』するのです。

この原則を守れば、先頭で提示したマスタテーブルの問題も簡単に答えを導けます。

なお、なぜ上記の原則が正しいのかは、ここでは深く追求しませんのであしからず

マスタテーブルはhasOneではなくhasMany

答えを先に言ってしまいました。
それでは順を追って、なぜそうなるのかを説明していきたいと思います。

例の確認

  • ブログの記事を扱ったArticleモデルと、その記事のステータスを扱うStatusモデルがある
  • ArticleモデルとStatusモデルのアソシエーションを検討する
  • Statusモデルは、editing , published , deleted の3レコードのみからなるマスタテーブル

選択肢はみっつ

hasOneと仮定した場合

  • Article hasOne Status
  • Status belongsTo Article

hasManyと仮定した場合

  • Article hasMany Status
  • Status belongsTo Article

belongsToと仮定した場合

  • Article belongsTo Status
  • Status has??? Article

1: 上記の原則を当てはめて考える

まず、外部キーをどちらに設定するかを考えます。

ArticleかStatusか、どちらになるのかは、あまり考える必要が無いでしょう。
マスタテーブルに外部キーを設定する可能性はありませんから。
つまりStatusには外部キーが存在しないと言うことになります。

ということは。
Article belongsTo Status が確定します。

2: Status has??? Article は、hasOne ? hasMany ?

逆に言えば、Status has??? Article ということになります。

では、hasOneなのかhasManyなのかが問題となりますが、この答えも自明でしょう。
記事は無限に増え続けるわけですから、hasManyでないと、statusが足りません(笑)
Status hasMany Article が確定します。

3: ふたたび上記の原則を当てはめて検証する

結果的にhasManyを用いて、以下のようになることが分かりました。

  • Article belongsTo Status
  • Status hasMany Article

当初イメージした『ArticleがStatusを持っている』という、やってしまいがちな解釈からいくと、逆になりましたね。

では、外部キーがどうなっているかを考えましょう

  • Article
    • id
    • status_id
    • title タイトル
    • body 本文
    • Other その他
  • Status
    • id
    • name

はい。間違いなく、上記の原則に沿っています。

それでも新人くんが迷っていたら

1: hasOneは関係を逆にしてもhasOne

hasOneかhasManyかですが、次のような考え方も成り立ちます。
CakePHPのアソシエーションには『1対多』は存在しても『多対1』はありませんよね。
一方でhasOneは『1対1』ですから、User hasOne Profile が存在する場合。それは、Profile hasOne User でもあるわけです。

つまり『関係性をひっくり返してみて成り立つかどうか』で、A hasOne B か A belongsTo B にするべきかを判断することが可能といえます。

上述のArticleとStatusの例で、Article hasOne Status と(間違って)考えたとしても、逆の関係性=『Status hasOne Article が成り立たないから間違っている』と判断することが可能です。
そんなときはArticle hasMany Status なんて突拍子もないことを考えることなく、Article belongsTo Status ( = Status has??? Article)と考えることができますね。

2: hasMany の場合は、Counter Cache を考える

Article hasMany Comment というアソシエーションがあるとき、Article モデルに comment_count というフィールドを設置し、Commentモデルの(belongsTo)アソシエーション時にcounterCache => trueとすると、commentsテーブルにコメントが追加されたり削除されたりするたびに、自動的にArticle.comment_count にコメント数が入ります。

Articleモデル単体を呼び出すだけで、コメント数が分かるようになり、非常に便利です。

Commentモデルを一緒に引っ張ってきた場合でも、すべてのコメントを取得することなくコメント数が分かります

上述の例に戻ると、たとえば『各ステータス別の記事数が知りたい』という場合、以下のようにするでしょう。

# Status Model
# * id
# * name
# * article_count
$statuses = $this->Status->find('list', array('fields' => array('Status.name', 'Status.article_count'));
debug( $statuses );
// example
// editing => 3
// published => 35
// deleted => 7

Status hasMany Article が正解ということは、Counter Cacheが可能であることからも分かりますね。

まとめ

アソシエーションの基本原則

  • 外部キーを持っているときはbelongsTo
  • hasOneおよびhasManyの相手方のモデルには外部キーが存在する

迷ったら

  • マスタテーブルはhasMany
  • 外部キーから考える
  • hasOneで良いのか迷ったら、関係性を逆にしてhasOneが成り立たなかったら、belongsTo
  • hasManyに違和感があったら、Counter Cache が設定できないかを考え、設定できればhasManyで問題ない

以上です。

アソシエーションは油断すると中級者でも混乱します。
CakePHPに不慣れな新人さんが、アソシエーションで迷っていたときの参考に是非。

参考

関連: モデルを結びつける :: モデル :: CakePHPによる開発 :: マニュアル :: 1.2 Collection :: The Cookbook
【CakePHP】アソシエーションで迷ったらこう考えよう | ECWorks Blog

プロフィール

@mon_sat

CakePHPをよく利用しています。

理解の浅かった半年前と、何も知らなかった一年前の自分への教科書として書いています。
当たり前のことも平易に。

RSS2.0

カテゴリ別エントリ一覧

タグ別エントリ一覧

アーカイブ