2012年6月17日日曜日

cakePHP2.1でjoinーhasOneを使う

前回で大雑把にはまとめてみたけど、今回からしばらく、アソシエーションに関して個別で詳しくまとめてみようと思う。多分ここを使いこなせるようにならないと本格的なウェブアプリは作成できないと思うし。

今回は、一番の基礎である(っぽい)hasOneについて。

hasOneのくっつき方

一対一の結合をする。$hasOneの設定があると、cakePHPは(LEFT) JOINを使って結合を行う。

たとえば、以下のようなテーブルがあるとする。

create table blog_users(
  `id` int(11) unsigned not null auto_increment ,
  `name` varchar(30) collate utf8_bin not null ,
  `address` varchar(100) collate utf8_bin not null ,
  primery key (`id`) 
)Engine=Innodb;

create table blogs(
  `id` int(11) unsigned not null auto_increment ,
  `blog_user_id` int(11) unsigned not null ,
  `published` tinyint(1) unsigned not null ,
  `title` varchar(100) collate utf8_bin not null ,
  primary key (`id`)
)Engine=Innodb;

Blogモデル(blogs)blog_user_idというBlogUserモデル(blog_users)の外部キーを持っている。これはcakePHPの命名規約に沿っていて、命名規約に沿ってる限りは色々な設定を簡略化して記述することができるようになる。

[補足]
上記のpublishedはtinyint(1)という設定にしてるんだけど、cakePHPではこの設定の場合はブーリアン値として認識される。findでもsaveでも、Formヘルパーでも0か1以外の入力はできない(それが嫌な場合、tinyint(2)以上にすれば回避できます)。

使う前の準備

アソシエーションを使う前には、全ての使用モデルを作成しておく必要がある。

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

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

テーブル名が命名規約に沿っていれば、最初これだけで大丈夫。

外部キーのインデックスを作成する

もうひとつ、事前準備としてMysqlで外部キーのインデックスを作成した方がいい(と思う)。上にも書いたように、hasOneでは(LEFT)joinを使う。例えば上のテーブルの場合、以下のようなSQLが発行される。

Select (中略) from blog_users as BlogUser left join blogs as Blog on Blog.blog_user_id = BlogUser.id Where (略)

ここでblog_user_idにインデックスが張られていない場合、基本的にblogsは毎回フルテーブルスキャンが発行されてしまってパフォーマンスが大きく低下してしまう。テーブルが小さい場合はそこまで問題ないけど、量が多くなると発行に1分以上かかったりするようになっちゃう。

というわけで、以下のようにインデックスを貼っておく。
ちなみに普通のインデックスだけでも劇的にパフォーマンスは改善されるけど、1対1結合で重複しないとわかってるならユニークインデックスを貼る方がいい。

alter table blogs add unique index blog_user_id (blog_user_id);

これで事前準備は完了。

hasOneの宣言

hasOneでは相手側が外部キーを持っているモデルに使用できる。要は上記の場合、BlogUserモデルはBlogモデルとhasOneで結びつけることができるということね。

宣言する際には、以下のようなパラメータが使える。

//app/Model/BlogUser.php
<?php
  class BlogUser extends AppModel{
     public $hasOne = array(
        'Blog'=>array(
            'className' => 'Blog',
            'foreignKey'=> 'blog_user_id',
            'type'=>'left',
            'conditions'=> array('Blog.published'=>1),
            'fields' => array('Blog.*'),
            'order'=> array('Blog.id DESC'),
            'dependent'=> true
         )  
     );
  }
?>

■キー名(Blog)
この場合、エイリアス名となる。今回はBlogと書いてるけど、当然Bでも何でもいい。

■className
結合するモデル名。上のキー名がモデル名と一致してる場合は省略できる。

■foreignKey
結合先が持ってる外部キー。命名規約に沿ってる場合は省略可能。

■type
結合タイプをleftrightinnercrossの中から選べる。

■conditions
find()で指定する以外に、範囲を制限する場合に使用する。

■fields
抽出するフィールドを選択する。全部を選択する場合、nullにするかこのキー自体を省略する。
逆に全部いらない場合はfalseと設定する。

■order
結合した際にソートする場合に指定する。
hasOneの場合は、find()時でも設定してる場合そのに着く。ASCと書くと昇順DESCと書くと降順となる。

[補足]
あまりSQLに詳しくない人もいると思うので補足すると、前に着くか後に着くかは結構重要になる。
SQLの場合、ソートは記載された順から優先的にされる。
たとえば[BlogUser.id asc]が先に設定されていて、hasOneで[Blog.id desc]と設定した場合、最初にBlogUser.idで昇順ソートされ、BlogUser.idが同じ場合はBlog.idで降順ソートで返される

■dependent
trueとすると、選択モデル(この場合はBlogUser)のフィールドが削除された時それと同じ外部キーを持つ結合モデルのフィールドも削除してくれる。
デフォルトはfalse

省略して記載する場合

特に何も設定が必要ない場合(ただ結合すればいいだけなら)、命名規約に沿っていれば以下のように書くことができる。

public $hasOne = 'Blog';
//または
public $hasOne = array('Blog');

ただdependentは(よく削除する場合は)結構便利だから指定した方がいいかもね。その場合もこんな感じに省略してかける。

public $hasOne = array(
  'Blog'=>array('dependent'=>true) 
);

ただ、ぼくが神経質だからかデータベースで行の削除って断片化がおきそうな気がして極力したくないんだけど、そんなの心配するのってぼくだけなのかな。まあいいや。

なお、bindModel(アソシエーションを後から指定する)も同様な記載が可能です。

複数のテーブルを結合

ひとつのモデルに複数のモデルを同階層上にぶら下げる場合、単に配列として複数指定するだけで実装できる。もしもBlogUserBlogContentというような順で結合したい場合、$recursive = 2というように設定をすることで実装できる。

//同じ階層の場合
var $hasOne = array('Blog','Content');

//階層が違う場合
//BlogUser.php
var $hasOne = 'Blog';
var $recursive = 2;

//Blog.php
var $hasOne = 'Content';

[補足]
$recursiveを使った場合、返される配列は階層2のモデルの結果は階層1の中に入り込む。また、どうも(LEFT)JOINは使わずにクエリの分割によって実装するみたい。それが嫌な場合は、ちょっと強引だけど以下のように無理矢理設定することもできる。

//階層が同じ時と同じような配列の返し方をする
//BlogUser.php
var $hasOne = array(
  'Blog',
  'Content' => array(
     'conditions'=>array('Content.id = Blog.id')
   )
);
一対一という意味

cakePHP2.xCookBook(英語版)でも他の色んなサイトでもここでも、hasOneの結合の仕方を「一対一」と書いてるんだけど、これは一つの結果に対して一つの関連付けがされるって意味で、選択モデルの結果が重複しないという意味じゃない(そんなの皆わかってるか)。hasOneの結合は(LEFT)JOINを使っていて、基本はbelongsToと変わらない。

たとえば以下のような場合、結果はこうなる。

//blog_usersテーブル
+----+------+
| id | name |
+----+------+
|  1 | 太郎 |
|  2 | 二郎 |
+----+------+
//blogsテーブル
+----+--------------+-------+
| id | blog_user_id | title |
+----+--------------+-------+
|  1 |             1| テスト|
|  2 |             1| テスト2|
|  3 |             2| テスト3|
+----+--------------+-------+

//find('all')をした場合
Array(
 [0]=>Array(
   [BlogUser] => Array([id] => 1  [name] => 太郎)
   [Blog] => Array([id] => 1 [blog_user_id] => 1 [title] => テスト )
  )
 [1]=>Array(
   [BlogUser] => Array( [id] => 1 [name] => 太郎 )
   [Blog] => Array( [id] => 2 [blog_user_id] => 1 [title] => テスト2 )
  )
 [2]=>Array(
   [BlogUser] => Array([id] => 2 [name] => 二郎 )
   [Blog] => Array([id] => 3 [blog_user_id] => 2 [title] => テスト3 )
  )
);

もちろん、最初からhasOneでしか保存を扱わない、またはBlogに直に挿入したりしない限りこういうことにはならないけどね。ただ、動作はbelongsToと変わらないって考えといた方がいいかも。違いは外部キーを持ってる方で設定/呼び出しすることと、counterCacheという便利そうなのがbelongsToでは扱えるってことくらいかな。

長くなったんで、今回はこれくらいで。次回はhasOneで具体的に何かまた別のものを作ってみます…って思ったんだけど、多分hasOneだけでアソシエーションを構築するってことは皆無そう。だって、1対1の結合なら、普通にデータベースにフィールドを追加すればいいだけだもんねぇ。他のアソシエーションで構築した際の個別の検索や、転置インデックス構築の際には使いそうだからあくまでも補助としてある感じですね。あとは実際運営を始めてデータが膨大に入った後でフィールドを追加する必要に迫られた時とかね。何度かそういう経験あるんだけど。
転置インデックスでの使用はこの前へたくそながらやってみたんで、そこを見てみて下さい。

【参考リンク】
cakePHP2.xCookBook(英語版)

0 件のコメント:

コメントを投稿