CakePHP2で複数テーブルをJOINしながらfindする際のプログラミングとして、Containbleとrecursiveのどちらを使うかに関するTipsを残しておきます。
前提条件
ブログ記事を複数人のライターで投稿できる、CakePHP2でできたアプリケーションがあったとします。データベース構成はこんな感じ:
- admins:
- システム管理者。ライター管理や記事管理等のあらゆる権限を有する。
- writers:
- ブログ記事のライター。ブログ管理画面にログインし、記事を書くことができる。
- articles:
- 記事。大き分けてタイトル・導入文・コンテンツ文章の3つで構成される。どのライターが書いたかが分かるよう、writer_idを有する。
- uploaded_images:
- 記事に挿入する画像。各記事はアイキャッチ画像だけ、articlesテーブルのuploaded_image_idで指定する仕様になっている。
- categories:
- 記事カテゴリー。記事は複数カテゴリーに所属する可能性を考慮し、多対多の構成(articles_categoriesテーブル)をとっている。
- tags:
- 記事に付けられるタグ。記事は複数タグを付けられるよう、多対多の構成(articles_tagsテーブル)をとっている。
- comments:
- ブログ訪問者が記事に投稿できるコメント。showカラムはデフォルト値が0(非表示)で、ライターもしくはシステム管理者が承認(0→1)することで、初めてブログ上に表示されるようになる。
やりたいこと:「articles.id=21の記事情報と、その記事を書いたライター情報だけをfind()したい」
Modelの記述は
1 2 3 4 |
class Article extends AppModel { public $belongsTo = ['Writer', 'UploadedImage']; } |
であると仮定します。
この状態でController内に記述するとしたら、以下2通りあります。
1.recursiveを指定してfind()
1 2 3 4 |
$article = $this->Article->find('first', [ 'recursive' => 0, 'conditions' => ['Article.id' => 21] ]); |
2.Containable Behaviorを利用してfind()
1 2 3 4 5 |
$this->Article->Behaviors->load('Containable'); $article = $this->Article->find('first', [ 'contain' => ['Writer'], 'conditions' => ['Article.id' => 21] ]); |
1.2.どちらでも記事情報+ライター情報を取得することが可能です。
Containable Behaviorのほうが使いやすい
結論を言うと、Containable Behaviorでfind時のアソシエーションをコントロールするほうが便利です。理由はrecursiveの特性にあります。
recursiveは、値によってどこまでのアソシエーション階層のtableをJOINさせるかを決めるパラメータである
たとえば、
- recursive=-1:
- 自分のtableからのみSELECT
- recursive=0:
- 自分のtable + belongsToのtableをJOINしてSELECT
- recursive=1:
- 自分のtable + belongsToのtable + hasManyのtableをJOINしてSELECT
…といった具合に、値が大きくなるほど多数の階層のtableをJOINの対象にしていくのです。
つまり、
階層単位でJOINするテーブルをコントロールするため、場合によってはあなたが必要としないテーブルまでJOINさせてしまうことがある
というわけです。今回の要件を改めて見てみると、
「articles.id=21の記事情報と、その記事を書いたライター情報だけをfind()したい」
となっています。1.のrecursiveを0に指定してfind()をすると、writersだけでなくuploaded_imagesまでJOINしてしまうことになります。SELECT文発行時に不要なtableをJOINしていることになるので、
DBへの負担とPHPのメモリ使用量を無駄に消費している
ことになってしまいます。
Containable BehaviorはJOINさせるtableを直接指定することが可能
2.では事前に「Containable Behaviorを使いますよ〜」という宣言をするため、
1 |
$this->Article->Behaviors->load('Containable'); |
上記のようにload()をした後にcontainが使えるようになります。Containable Behaviorの詳細な使い方は公式ドキュメントに譲るので、確認していただければと。
recursiveを使うのをやめて、Containable Behavior一択でいこう
このContainable Behaviorは、データベース内のテーブル数が少なかろうと多かろうと関係なく、recursiveに比べて使い勝手が圧倒的に良いです。
そのため、もしCakePHP2で0からアプリケーションを構築しようと思っているのであれば、AppModel内に
1 2 3 4 |
class AppModel extends Model { public $actsAs = ['Containable']; } |
と入れておくほうが良いと考えています。こうすることで、Controller内で都度load()を呼び出す必要が無くなるからです。
もし既存アプリケーションで、且つ複数開発者によってcontainとrecursiveの利用が統一されていなかったら
containを使用する方向に寄せて、recursiveを使用している箇所をcontainの記述に修正していくことをオススメします。
完了したら、AppModel内で
1 |
public $recursive = -1; |
と記述し、根本的にrecursiveを使っていないことを明示しておきましょう(CakePHP2の公式ドキュメントでも推奨されています)。
また、社内コーディング規約として
「find()等でJOIN時はrecursiveではなく、containを使うこと」
というルールを作り、エンジニアチーム内に明示しましょう。ルールにしておけば、仮に外部企業に機能改修を依頼することがあった時、予めルールを先方に共有しておくことで品質担保に繋がります。