ECWorks Blog

ECWorks Blog

CakePHPを中心としたサイト開発情報をメインに公開。新しもの好きなので時々製品レポートなんかも。

【CakePHP】アソシエーションで迷ったらこう考えよう

cake-logoCakePHPの機能でまず使ってみたくなるのが「アソシエーション」でしょう。アソシエーションはテーブルのJOINをもう少し概念的にしたもので、理解できるとクエリが格段にわかりやすく表現できます。
ところが、理解するまでに結構苦労してしまう事も確かです。そこで、実践的な部分も踏まえて、もう少しわかりやすい解説をしてみようと思います。

■模範的な考え方

CakePHPマニュアル等でまず解説されているアソシエーションは、次の通りです。

  • hasOne (A hasOne B) Aは1つのBを持っている
  • hasMany (A hasMany B) Aは複数のBを持っている
  • belongsTo (B belongsTo A) BはAに従属している
  • hasAndBelongsToMany(HABTM) (A HABTM B) AとBは複数のそれぞれを持っている

文章に書いただけでは分かりにくいいかもしれません。
図式化したものを以前に公開しましたので、 こちら も併せてお読みください。

■簡単な考え方

テーブルを設計する際、慣れないうちは「外部キー」をどのテーブルに置くか迷ってしまうことがあると思います。

実は、hasOneとhasManyはほとんど似通った関係で、複数になったときのソートであるとか、リミットであるとか、そういった「複数由来の機能」がhasManyに余分についているのみです。なので、外部キーに関してのみに言及すれば、hasOneとhasManyは同じです。
前置きはこのくらいにしておいて、外部キーの関係は「has???」「belongsTo」「HABTM」の3パターンがあり、このうちhas???は「相手に外部キーがある」で、belongsToは「自分に(相手の)外部キーがある」と覚えると簡単です。

問題はHABTMで、理解に苦しまれている方も多いかと思いますが、実は「A HABTM B」は、上記の理屈でいくと「AもBもhasMany」なのです。「じゃあbelongsToに当たる部分は?」と思われると思いますが、HABTMを実現させるためにはA/B各テーブルの他に、ABテーブルが必要になり、これがbelongsToとなります。つまり、

A HABTM B =
(A hasMany AB) and (B hasMany AB) / (AB belongsTo A) and (AB belongsTo B)

となります。このため、ABテーブル内に、AとBの外部キーを持っているのです。
なお、ABテーブルは、AとBにHABTMを指定することで「ABテーブルが存在することになっている」ため、ABモデルは記述不要です。また、上記のようにAとBにhasManyを、ABにbelongsToをしても、同じように動作できます。

■主従関係をどう決めたらいいか?

模範的な考え方がCakePHPマニュアルで提示されているため、そのまま主従関係を厳密に考えたくなるのはよく分かります。もちろん、それは模範的な考え方であるため、当然OKです。
ですが、ここでは更に別の視点からも考えてみることにします。

一度この主従関係を取り払って考えてみると、それぞれにこのような特徴があります。

  • A hasOne Bの場合、Bは1つである
  • B belongsTo Aの場合も、Bは1つである
  • A hasMany Bの場合は、Bは複数である

そして、外部キーの関係は

  • A hasOne Bの場合、Bは外部キーを持っている
  • A belongsTo Bの場合は、Bは外部キーを持っていない
  • A hasMany Bの場合は、Bは外部キーを持っている。

となります。
書き換えると…

  • Bが1つであり、外部キーを持ちたくない場合は「B hasOne A」にする
  • Bが1つであり、外部キーを持ちたい場合は「B belongsTo A」にする
  • Bが複数の場合は「A hasMany B」にする
  • BもAも複数の場合は「A HABTM B」にする

とすることが出来るのです。つまり主従関係前提でなく、逆に考えて「外部キーを何処に置くか」というテーブルの都合でアソシエーションを決めることも出来る、というわけです。
ちなみに、自分に外部キーを置くことで相手を複数存在させることは、物理的に出来ません(よく考えてみましょう)。

■実践的なアソシエーションの考え方

例えば、「ユーザデータの中に、ステータスを持たせたい」場合を考えてみます。ステータスを数値にしてしまう場合もありますが、ソースコードに依存しない形で組み込みたい場合は、ステータスデータもデータベースにマスタデータとして格納するのが一般的です。

この場合、それぞれUserモデル、Statusモデルとすると、模範的な考え方をすると、「Userは1つのStatusを持っている」としたくなると思います。ところが、このままの考え方ですと「同じ意味を持つステータスが複数存在する」事になります。例えば「会員」「非会員」というステータス項目(文字列または番号)があるとすると、全部のUserに、独立したデータとして存在することになってしまいます。
ステータス項目を単一のデータとして認識させるためには、ステータス項目をモデルとして実現させる、つまりStatusMtrというモデルが必要になりそうだと考えます。「StatusはStatusMtrを1つもつ」、逆に「StatusMtrはStatusに所属している」という具合です。ところが今回の趣旨でいきますと、Statusは複数のStatusMtrから呼ばれることになりますので、逆に言いますと「StatusMtrはStatusを複数持つ」と考えなければいけません。

つまり、

User hasOne Status / StatusMtr hasMany Status
Status belongsTo User / Status belongsTo StatusMtr

というアソシエーションにすることになります。主従関係で表すと、「UserはStatusを介してStatusMtrを一つもつ」事になります。

しかし、主従関係を入れ替えてしまうことで、Statusモデルが不要になります。つまり「StatusMtrは複数のUserをもち、UserはStatusMtrに所属する」事にしてしまえばいいのです。UserにStatusMtrの外部キーを持たせてしまうことで、StatusMtrは複数のUserから呼ばれることができ、逆にUserは1つのStatusMtrを示すことが出来ますから、今回の趣旨に合致します。
何故このように考えられるのかというと、そもそもステータスはUser内で知りたい情報ですから、別に切り分ける必要がないからです。ただ、値ではなくいろいろな情報を持たせたい、冗長を避ける目的でテーブルを切り分けている訳で、情報そのものは本来はUserにあるべきです。そういった発想で、意味をもった「値」ではなく、外部キーをUserの中に置くことも理にかなっていると言えます。

ちなみに、主従関係をひっくり返しても、問題なく関連データを引っ張ってこれます(でなければ双方向で定義する意味がありません)。
主従関係としてしまうことでアソシエーションのメカニズムをわかりやすく説明することが出来ますので、マニュアルはこのような書き方になっていると思いますが、これはあくまでも1つの概念であり、本質はそれを構成するメカニズムです。メカニズムを正しく理解していれば、更に自在にコーディングすることが出来る、という訳なのです。

【追記】
修正追加コメントは削除して整理しました。(2009.3.6)


Tagged as: , ,

5 Comments

  1. CakePHP勉強中なので、とても参考になります!

    先生執筆の「WebデザイナーのためのCakePHPビューコーディング入門」はすごくわかりやすくて文章も親しみやすく、勉強になりました。

    最新作「詳解CakePHP辞典2.0/2.1/2.2/2.3対応」が明日Amazonから届く予定で、とても楽しみです。

    これからもよろしくおねがいします!!

  2. ありがとうございます!

    先生と呼ばれるにはとても恥ずかしい内容ですが、お役に立てて何よりです。
    「詳解CakePHP辞典」「CakePHPビューコーディング入門」とあわせてこれからもご愛顧いただけますと幸いです。