Re: 中間テーブルのcreated、modifiedについて
イントロダクション
CakePHPのhasAndBelongsToMany(以下habtmと呼ぶ)機構は、期待する振る舞いをしないことがあります。
CakePHPはそのマニュアルで、habtmが複雑になったときについてこう言及しています:
http://book.cakephp.org/ja/view/1034/Saving-Related-Model-Data-HABTM
また、join table (結合の情報を記述するテーブル) に (関係の作成時刻や、メタデータなどの) フィールドを付け加えたい場合にも言及しておきましょう。HABTM の join table に付け加える事もできますが、もっと簡単な方法もあります。
2つのモデルの HABTM 関係は、本当は、hasMany と belongTo による3つのモデルの関係を省略して表したものです。
このように、hasManyとbelongsToに分解することを推奨されます。
しかし、モデルとフォームが密接に関わりあってるため、これをすると、habtmの為に用意された機構が使えなくなります。
- data[HabtmModel][HabtmModel]という構造
- FormHelper::input('HabtmModel'); といった書式
- 自動的なリンクテーブルの更新(新しい関係性のみが保存される)
これらにより、分解したはいいがどうも使いづらい、どうすればいいんだ、という意見が沸いてくるのは当然です。
しかし、分解したからといって、アプリケーション側でその機構を作れないわけではありません。
本来のhabtmと同じような振る舞い
<?php class UsersController extends AppController { public function add(){ if (!empty($this->data)) { if (is_array($groups = $this->data['GroupsUser']['GroupsUser'])) { foreach ($groups as $group_id) { // 整形 $data = array(); //省略。 } } $this->User->saveAll($data); // do something } } }
FormHelperの挙動を詳しく追いかけると、望むような形式で返してくれることは、残念ながらありません。
一つの方法として、このように整形することを考え付くでしょう。
しかし、明らかにfatコントローラです。edit()の時もコピペになること間違いなしです。
これらのことは、モデル側で吸収すべきです。
さて、ごちゃごちゃ言っても始まらないので、自分なりの一つの答えを提示します。もちろん、これが唯一の正解ではないということをご理解ください。
以下、
User => Team <= Group
User:
- id
- name
Gourp:
- id
- name
Tema:
- id
- user_id
- group_id
とします。
コントローラは、bakeしたものから、次のコードをadd, editの末尾に付加するだけです。
<?php $this->set('groups', $this->User->Team->Group->find('list')); ?>
Groupモデル、TeamモデルもBakeしたもので良いです。
Userモデル:(基本はやはりBake)
<?php class User extends AppModel { var $name = 'User'; var $displayField = 'name'; //The Associations below have been created with all possible keys, those that are not needed can be removed var $hasMany = array( 'Team', ); function afterSave($created) { $groups = @(array)$this->data[$this->alias]['group_id']; if (!empty($groups)) { $old = $created ? array() : $this->Team->find('list', array('fields' => array('Team.group_id'))); $user_id = $this->id; foreach (array_diff($groups, $old) as $group_id) { $this->Team->Group->id = $group_id; $this->Team->create(compact('user_id', 'group_id')); if (!$this->Team->Group->exists() || !$this->Team->save()) { return false; } } if (!$created) { $this->Team->deleteAll(array('Team.group_id' => array_diff($old, $groups))); } } elseif (!$created) { $this->Team->deleteAll(array('Team.user_id' => $this->id)); } } function afterFind($results, $primary = false) { if ($primary) { foreach ($results as $index => $result) { $results[$index] = $this->normalizeResult($result); } } return $results; } // edit()の為の整形 function normalizeResult($result) { if (!empty($result['Team'][0]['group_id'])) { $result[$this->alias]['group_id'] = Set::extract('/Team/group_id', $result); } return $result; } }
add.ctp(edit.ctp)
<div class="users form"> <?php echo $this->Form->create('User');?> <fieldset> <legend><?php __('Add User'); ?></legend> <?php echo $this->Form->input('name'); echo $this->Form->input('group_id', array('multiple' => 'multiple')); // これを付け加えただけ ?> </fieldset> <?php echo $this->Form->end(__('Submit', true));?> </div> <?php // アクションリストは省略 ?>
基本はafterSave()とafterFind()で対応ということです。
おっと、これを見て以下のような疑問をもったら鋭いです:
- 一々こういうふうに書かなきゃいけないの?だるいな・・・
- Groupモデルも同じようにしたいんだけど・・・
- afterSave()で失敗したらfalse返してるけど、意味無いよね?トランザクションどうするの?
まず1. と2. に関してです。CakePHPにはBehaviorという恐ろしく便利なものがありますよね。
あとは言わなくてもわかると思います :)
3. に関しては、return falseの代わりにExceptionを投げましょう。
PHP4の場合は、Model::onError()をうまく使えば制御はできます。
HABTM Add & Delete Behavior
http://bakery.cakephp.org/articles/bparise/2007/05/09/add-delete-habtm-behavior
以上の解説のように、本来のhabtmの挙動とはまた違った挙動にしたい(例えば、既存のものはそのままで、新しい関係を追加(or削除)だけしたい場合も、ビヘイビアにまとめるのがいいでしょう。
上記のものは分解することなく実現していますが、概念だけは押さえておきましょう。