2012年6月15日金曜日

cakePHP2.1でJoinする

今回は単一でのエントリ。hasOneを使ってみる際にテーブル結合の方法など色々と調べたので、それらをいくつかまとめてみる。
(まだ網羅するまでには調べてないから、あくまでも調べた範囲内でだけ。いつかまとめたいな)

テーブルをJoinするには

hasOne、belongsTo、hasMany、hasAndBelongToManyを使うことでテーブル結合を実装できる。
cakePHPでモデル(=DBテーブル)を簡単に結合できる方法としてこれらが用意されてる。

ちなみに、まとめてアソシエーションっていうらしい。直訳すると関連、または連合だね。多分関連という意味だろうけど、連合という言い方の方がかっこよくて中二病心をくすぐってくれるからその意味で使っていこう。観念連合っていう意味でもあるらしい。

アソシエーション結合タイプ外部キー結合方法
hasOne1対1の結合相手が持つ(left)join
belongsTo多対1の結合自分が持つ(left)join
hasMany1対多の結合相手が持つクエリ分割
hasAndBelongsToMany(HABTM)多対多の結合第三者が持つクエリ分割

書いてはみたけど、これだけじゃさっぱり解らないですやね。上の表の外部キーの部分は、マニュアルは非常に分かりづらかったからちょっとでもわかりやすくって思ってこう書いてみた(やっぱり分かりづらいけど)。
自分──hasOneなどを使うモデル
相手──アソシエーション先
第三者──仲介テーブル(使い方は下記に記載)。

hasManyHABTMはクエリが分割されて発行される。その方が余計なメモリを食わない分効率はいい。ただ、その為conditionsの指定によってはエラーが起きる

命名規約などのルール
  • 外部キーは結合モデル_id(アンダースコア型)にする
  • HABTMテーブルは二つのテーブルをアルファベット順&アンダースコア型でくっつける

HABTMのはわかりづらいけど、たとえばusersテーブルtagsテーブルをくっつけるHABTMテーブルを作成する場合、tags_usersという名前で作成する。

他の結合方法

実は、これらを使わなくても普通にfind時にJOINもできる。やり方は簡単で、パラメーターの中に'joins'という配列を入れればいいだけ。
他にもModel::query()という直にSQLをかける関数も用意されてて、その中で普通にjoinを記述することもできる。

アソシエーションを使うメリット

そのまま他の結合方法を使うデメリットにもなるんだけど、

  • 非常に簡単に結合できる
  • クエリを発行する度に書く手間が省ける
  • beforFind、afterFindが自動で適応される ※注
  • 他cakePHPの便利機能をフル活用できる
  • フィールド名とか書かなくてもちゃんと入ってくれる
    (joinsでやると指定しないと入らない)
beforeFind、afterFindに関して

一応joinsを使っても、そのモデルを使った場合はbeforeFind、afterFindは適応される。でも、アソシエーションを使った場合、afterFindはそれぞれのモデルで設定されたもの発行されるみたい。たとえば前回hasOneを使った際、どちらも普通に適応されてた。

afterFindではデータの再フォーマットをする機会が殆どになると思うんだけど、この機能は発行の度に再指定する必要がなくなるから便利ですね。

あ、ただbeforeFindはその選択中のモデルのしか適応されないみたい。

アソシエーションがある場合のfind()の動き方

ついでだから調べてみた。
たとえば1つのテーブルをhasOneでアソシエーションした場合、以下の動きをするみたい。

  1. 選択モデルのbeforeFind()呼び出し
  2. アソシエーションの結合 ※1
  3. クエリデータ作成 & $this->getDataSource()->read()メソッド呼び出し
  4. 結果を返す
  5. アソシエーションモデルのafterFind呼び出し ※2
  6. 選択モデルのafterFind呼び出し ※2

※1 正確には、3と4のあたりでアソシエーション設定を確認して結合される
※2 同じく3と4で結果を作成してる時にそれぞれ呼び出されるみたい。

色々と試してみたんだけど、最初に選択モデルのbeforeFind()が呼び出され、最後に選択モデルのafterFindが呼び出された。アソシエーションモデルのafterFindが先。
また、(5)アソシエーションモデルのafterFindが呼び出される時には、どうも選択モデルの結果は入ってないみたいで、もし選択モデルのフィールドをアソシエーションモデルのafterFindで変更しようとすると失敗する。(6)が呼び出される時には両方セットされた状態でafterFindがかけられる。

あ、選択モデルっていうのは$this->Test->find()みたいにコントローラ内で検索をかける時のモデルのことね。念のため。

実際に結合する場合

非常に簡単だった。モデルのクラスの下に、以下のように変数を設定するだけでcakePHP側で自動的に認識してくれる。
(ただし命名規約にしたがってる必要あり)

【基本型】
var $hasOne = array('結合モデル');
var $hasMany = array('結合モデル');
var $belongsTo = array('結合モデル');
var $hasAndBelongsToMany = array('結合モデル');
【具体例:hasOneを使用する場合】
//app/Model/User.php
//Campanyモデルと結合する場合
//campaniesにはuser_idがある
<?php
  class User extends AppModel{
    var $hasOne = array('Campany');
  }
?>

//app/Controller/UsersController.php
<?php
  class UsersController extends AppController{
     //var $uses = array('User','Campany');
     //↑Userモデルは命名規約で自動的に使え、CampanyもJoinで使うだけなら指定する必要ない
     function index(){
         $result = $this->User->find('all');
         //$resultにはcampaniesも結合された状態で返される
     }
  }
?>

あとは、普通に選択したモデルでfindなどを使って取り出せばいいだけ。

もちろん個別で細かく設定をすることもできる。今回は基本的なところだけまとめておきます。

【他の設定方法】
//hasOneでの設定だけど基本的にHABTM以外は他のも全部同じ。

var $hasOne = array(
   'C' => array(
      'className'=>'Campany',
      'foreignKey'=>'user_id'
    )
);
//この場合、Cはエイリアス名になる。

var $hasOne = array(
  'C'=>array(
     'className'=>'Campany',
     'foreignKey'=>false ,
     'conditions'=>array('C.id = User.id')
   )
);
//この設定の仕方をすれば、複合プライマリキーにも対応できる(かも)
後から指定する/後から解除する

詳しいことは「郵便番号検索を作る─5.住所検索できるようにする(後編)」でも書いてます。
bindModel()で設定、unbindModel()で解除可能。

//例:コントローラ内(当然モデル内でも同様にできる)
//後から設定する
$this->User->bindModel(
   array('hasOne'=>array('Campany'))
   ,false
);
//または
$this->User->bindModel(
   array(
     'hasOne'=>array(
        'C'=>array(
           'className'=>'Campany',
           'foreignKey'=>'user_id'
        )
      )
   )
   ,false
);
//第2引数をtrueに設定すると、1回の検索でのみの適応となる。
//デフォルトはtrueだから、永続的に設定する場合はfalseに指定する必要がある。
//##############################################################
//後から解除する
//第2引数はbindModelと同じ
$this->User->unbindModel(
  array(
    'hasOne'=>array('Campany') 
  )
  ,false
);
hasAndBelongsToManyの設定方法

hasAndBelongsToMany(HABTM)では多対多のいわゆるタグ付けみたいなのが簡単に実装できる。たとえばUserモデル(users)とTagモデル(tags)でHABTMを実装する場合、tags_usersという専用のテーブルを作成する。テーブルの基本構成は以下の通り。

tags_users
id主キー
user_idUserモデルの外部キー
tag_idTagモデルの外部キー

モデル名はTagsUserとなる。app/Model/TagsUser.phpを作成した後、Userモデルに以下のように記載すれば、命名規約に沿ってる場合自動で設定される。

//app/Model/User.php
<?php
  class User extends AppModel{
     var $hasAndBelongsToMany = array('Tag');
  }
?>

非常に簡単でつね。Tagモデルで使用する場合は、上の値は'User'でいい。
使用例はWEBOPIXELさんのトコに詳しく載ってました。
(これはCake1.xのやり方みたいで、2.1で行うには少し変更する必要があるみたい)

もちろん基本はこれでいいんだけど、これだけの設定の場合、保存したりする度に内部的に

  1. 毎回HABTM内の既存のフィールドを削除
  2. 再び新規作成

というような、何かいやらしい動きをする。主キーの値はどんどんと繰り上がっていって気分的に嫌だ。

それを少しでも抑える方法として、パラメーターのuniquekeepExistingと指定すると必要のない箇所だけ削除って方法をとってくれ、多少は主キーの繰り上がりは抑えられる(cakePHP2.1以降?に追加された機能です)。

//app/Model/User.php
<?php
  class User extends AppModel{
     var $hasAndBelongsToMany = array(
          'Tag' => array(
             'unique'=>'keepExisting'
           )
         );
  }
?>

これをデフォルトの書き方にした方がいいかも。

他の方法での結合──joins

上でも書いた通り、アソシエーションを使わなくてもjoinsパラメーターでも結合することはできる。
やり方は以下の通り。

【使用例】
$this->Model->find(
   'all' , array(
      'conditions'=>array('Model.id'=>1),
      'joins'=>array(
          array(
            'table'=>'join_table1',
            'alias'=>'Join1',
            'conditions'=>array('Join1.id = Model.id')
          ),
          array(
           ...
          ),
          ...
      )
   )
);

ただし、この方法だとcakePHPでの機能が制限されるフィールドを指定しないと値を取り出せないなど多少使い勝手が悪くなる。この方法は内部的に結合する時といったように、あくまでも補助として使用する程度に止めた方がいいかも。

アソシエーションを使った保存方法

アソシエーションを使った専用の保存メソッドとしてModel::saveAssociated($data)というのが用意されてる。
これを使えば、View::Formヘルパーなどで作成/投げられたデータをこの中にぶちこむだけでアソシエーションデータも保存してくれる。
(その際、アソシエーション先の主キーが含まれてないと新規作成となる)

簡単に作ってみる

最後に、hasOneで簡単に検索/保存まで実装してみる。

【テーブルの作成】
#Testテーブルを作成
mysql> create table tests (
    ->  id int(11) unsigned not null auto_increment ,
    ->  title varchar(30) collate utf8_bin not null default '',
    ->  primary key (id)
    -> )Engine=Innodb;
Query OK, 0 rows affected (0.27 sec)

#アソシエーションテーブルを作成
mysql> create table joins (
    ->  id int(11) unsigned not null auto_increment ,
    ->  test_id int(11) unsigned not null ,
    ->  comit varchar(30) collate utf8_bin not null default '',
    ->  primary key (id)
    -> )Engine=Innodb;
Query OK, 0 rows affected (0.11 sec)
【作成していく】
//[モデル]
//app/Model/Test.php
<?php
   class Test extends AppModel{
      var $hasOne = array('Join');
   }
>

//app/Model/Join.php
<?php
   class Join extends AppModel { }
>


//[コントローラー]
//app/Controller/TestsController.php
<?php
   class TestsController extends AppController{
      function index($id = null){
         //ポストデータがある場合保存
         if($this->request->data){
            if($this->Test->saveAssociated($this->request->data))
                $this->Session->setFlash('保存しました');
         }
         $this->request->data = $this->Test->read(null , $id);
      }
   }
>

//[ビュー]
//app/View/Tests/index.ctp
<H1>編集</H1>
<?php
   print $this->Form->create('Test');
   print $this->Form->input('Test.id');
   print $this->Form->input('Test.title');
   print $this->Form->input("Join.id");
   print $this->Form->input("Join.comit");
   print $this->Form->end('送信');
?>

ちゃんとトランザクションも使ってくれるんだね。


とりあえず、今回は大まかな点だけまとめてみた。これ以上長くなっても誰一人読んでくれなくなりそうだから、細かい設定とかそういうのはそれぞれ個別に、また気が向いたらまとめようと思います。

【参考リンク】
cakePHP2.xCookBook(英語版)
CakePHPのhasAndBelongsToMany(HABTM)をチェックボックスで関連付ける
【cakePHP】アソシエーションで迷ったらこう考えよう

0 件のコメント:

コメントを投稿