CakePHP hasAndBelongsToMany(HABTM)に超苦戦中!!!><

色々調べながらやっているのだけれど、どうもうまくいかない。
表示はさくっといくのだけれど、保存ができない。

解決はしていないが、苦悩の様子を書いておこうと思う。

【参考にしたサイト】

Model定義してsaveとかsaveAllとかすればいいだけとか、定義に注意すればいけるとか、そういうのはよく見るんだけどなぁ。

【環境】

まず、やってみる

HABTMを確認するための最低限の定義。こんなテーブルを作る。
f:id:myamyugon:20121204225036p:plain

SET SESSION FOREIGN_KEY_CHECKS=0;

/* Drop Tables */

DROP TABLE HOGES_TAGS;
DROP TABLE HOGE;
DROP TABLE TAG;

/* Create Tables */

CREATE TABLE HOGE
(
	ID INT NOT NULL AUTO_INCREMENT,
	TITLE VARCHAR(40) NOT NULL,
	CONTENTS TEXT,
	PRIMARY KEY (ID)
);


CREATE TABLE HOGES_TAGS
(
	HOGE_ID INT NOT NULL,
	TAG_ID CHAR(6) NOT NULL
);


CREATE TABLE TAG
(
	TAG_ID CHAR(6) NOT NULL,
	TAG_TEXT VARCHAR(20) NOT NULL,
	PRIMARY KEY (TAG_ID)
);

/* Create Foreign Keys */

ALTER TABLE HOGES_TAGS
	ADD FOREIGN KEY (HOGE_ID)
	REFERENCES HOGE (ID)
	ON UPDATE RESTRICT
	ON DELETE RESTRICT
;

ALTER TABLE HOGES_TAGS
	ADD FOREIGN KEY (TAG_ID)
	REFERENCES TAG (TAG_ID)
	ON UPDATE RESTRICT
	ON DELETE RESTRICT
;

タグデータを入れておく。

INSERT INTO tag (`TAG_ID`, `TAG_TEXT`) VALUES ('TAG001', 'たぐたぐ1'), ('TAG002', 'たぐたぐ2'), ('TAG003', 'たぐたぐ3');

テーブルに対するModelを作る。
入力チェックは何も入れない。最低限の定義で。

Hoge.php

<?php 
App::uses('AppModel', 'Model');

class Hoge extends AppModel
{
	public $name = 'Hoge';
	public $useTable = "HOGE";
	public $primaryKey = "ID";
	
	public $hasAndBelongsToMany = array(
		'Tag' => array(
			'className' => 'Tag',
			'joinTable' => 'HOGES_TAGS',
			'with' => 'HogesTags',
			'foreignKey' => 'HOGE_ID',
			'associationForeignKey' => 'TAG_ID',
			'unique' => true,
		),
	);
}

Tag.php

<?php
App::uses('AppModel', 'Model');

class Tag extends AppModel
{
	public $name = 'Tag';
	public $useTable = "TAG";
	public $primaryKey = "TAG_ID";
	public $displayField = 'TAG_TEXT';
}

HogesTags.php

<?php 
App::uses('AppModel', 'Model');

class HogesTags extends AppModel
{
	public $name = "HogesTags";
	public $useTable = "HOGES_TAGS";
	

	public $belongTo = array(
		'Hoge' => array(
			'className' => 'Hoge',
			'foreignKey' => 'ID',
		),
		'Tag' => array(
			'className' => 'Tag',
			'foreignKey' => 'TAG_ID',
			'fields' => array('TAG_ID', 'TAG_TEXT'),
		),
	);
}

#HogesTagsのbelogToや、HogeのHABTMのwithなんかは入れても入れなくても挙動は変わらなかったなぁ・・・

Hogeを編集するControllerも作る。
$scaffoldするだけにしてみる。

HogesController.php

<?php
class HogesController extends AppController 
{
	public $name = 'Hoges';
	public $scaffold;
}

そして、画面を表示。
http://localhost/hoges/

表示できたら、New HogeからHoge追加。
f:id:myamyugon:20121204233859p:plain

ちゃんと追加できてる。
f:id:myamyugon:20121204234034p:plain

Editで編集画面を開いてみると、選択されていない。なんで!?
f:id:myamyugon:20121204234230p:plain

hoges_tagsテーブルに直接データを突っ込んで編集画面を開くと、選択された状態になる。

INSERT INTO hoges_tags (`HOGE_ID`, `TAG_ID`) VALUES ('1', 'TAG001'), ('1', 'TAG003');

f:id:myamyugon:20121204234614p:plain

編集画面でこのまま保存すると、消える。
(´・ω・`)

うーむ・・・どういうことだ。
ちなみに、$scaffoldを使わずにbakeで生み出したControllerでも同様。saveとかsaveAllとか試したけど、ダメ。。。

Testで保存のテスト

ここでちょっとテストに色々書いて試してみる。
bakeでHogeのTestを作って、そこにtestHoge1メソッドを追加。

debug("ほげほげ");
$id = '1';
$this->Hoge->id = $id;
$this->assertNotEqual($this->Hoge->exists(), false, "ID=".$id."が存在しない");
debug($this->Hoge->read(null, $id));

http://localhost/test.php にアクセスしてテストを実行、デバッグログでHogeの中身を見ると

array(
	'Hoge' => array(
		'ID' => '1',
		'TITLE' => 'ほげタイトル',
		'CONTENTS' => 'ほげのコンテンツ。
ほげほげほげ。
'
	),
	'Tag' => array(
		(int) 0 => array(
			'TAG_ID' => 'TAG001',
			'TAG_TEXT' => 'たぐたぐ1'
		),
		(int) 1 => array(
			'TAG_ID' => 'TAG003',
			'TAG_TEXT' => 'たぐたぐ3'
		)
	)
)

なるほど。これをそのまま保存してみよう。

debug("そのまま保存してみる");
$data = $this->Hoge->read(null, $id);
$this->assertNotEqual($this->Hoge->save($data), false, "保存に失敗しちゃった。");
debug($this->Hoge->read(null, $id));

すると・・・

array(
	'Hoge' => array(
		'ID' => '1',
		'TITLE' => 'ほげタイトル',
		'CONTENTS' => 'ほげのコンテンツ。
ほげほげほげ。
'
	),
	'Tag' => array(
		(int) 0 => array(
			'TAG_ID' => 'TAG003',
			'TAG_TEXT' => 'たぐたぐ3'
		)
	)
)

あれ?タグ、足りなくね???

実行されたSQLを見ていくと・・・

Hoge本体を保存して

UPDATE `caketest`.`HOGE` SET `ID` = 1, `TITLE` = 'ほげタイトル', `CONTENTS` = 'ほげのコンテンツ。\r\nほげほげほげ。\r\n' WHERE `caketest`.`HOGE`.`ID` = '1'

タグの紐付けを削除して

DELETE `HogesTags` FROM `caketest`.`HOGES_TAGS` AS `HogesTags` WHERE `HogesTags`.`HOGE_ID` = 1 AND `HogesTags`.`TAG_ID` IN ('TAG001', 'TAG003')

紐付けをINSERTしなおす・・・っと。

INSERT INTO `caketest`.`HOGES_TAGS` (`TAG_ID`, `HOGE_ID`) VALUES ('TAG001', 1)

んで、もいっこ・・・って・・・え・????

UPDATE `caketest`.`HOGES_TAGS` SET `TAG_ID` = 'TAG003', `HOGE_ID` = 1 WHERE `caketest`.`HOGES_TAGS`.`HOGE_ID` = '1'

お前、なぜUPDATEする。なぜだ。

でも、これなら1つは保存されるはず。
画面では1つも保存されずに完全に消えるのだ。
なんでだろ・・・??

ひとまず、この時点で問題は2つできた。
1つは、取ってきたまま保存してもうまくいかないこと。
もう1つはControllerから保存した場合と挙動が違うこと。

まずは、取ってきたまま保存を成功させることを目標にしよう。

取ってきたまま保存できるようにする

これは本当に悩んだ。どうやってもできない。
何度も同じサイトを見て・・・気づいた!

CakePHP の HABTMって楽ですよ : かばだんなさん かく語りぬ2

紐付けテーブルに、idなんてものがある・・・
もしや・・・

SET SESSION FOREIGN_KEY_CHECKS=0;
DROP TABLE HOGES_TAGS;
CREATE TABLE HOGES_TAGS
(
	ID INT NOT NULL AUTO_INCREMENT,
	HOGE_ID INT NOT NULL,
	TAG_ID CHAR(6) NOT NULL,
	PRIMARY KEY (ID)
);
ALTER TABLE HOGES_TAGS
	ADD FOREIGN KEY (HOGE_ID)
	REFERENCES HOGE (ID)
	ON UPDATE RESTRICT
	ON DELETE RESTRICT
;
ALTER TABLE HOGES_TAGS
	ADD FOREIGN KEY (TAG_ID)
	REFERENCES TAG (TAG_ID)
	ON UPDATE RESTRICT
	ON DELETE RESTRICT
;

INSERT INTO hoges_tags (`ID`, `HOGE_ID`, `TAG_ID`) VALUES (NULL, '1', 'TAG001'), (NULL, '1', 'TAG003');

さっきのテストを実行!!
・・・
!!

array(
	'Hoge' => array(
		'ID' => '1',
		'TITLE' => 'ほげタイトル',
		'CONTENTS' => 'ほげのコンテンツ。
ほげほげほげ。'
	),
	'Tag' => array(
		(int) 0 => array(
			'TAG_ID' => 'TAG001',
			'TAG_TEXT' => 'たぐたぐ1',
			'HogesTags' => array(
				'ID' => '1',
				'HOGE_ID' => '1',
				'TAG_ID' => 'TAG001'
			)
		),
		(int) 1 => array(
			'TAG_ID' => 'TAG003',
			'TAG_TEXT' => 'たぐたぐ3',
			'HogesTags' => array(
				'ID' => '2',
				'HOGE_ID' => '1',
				'TAG_ID' => 'TAG003'
			)
		)
	)
)

そのまんま保存できたー!
でも・・・id邪魔だなぁ・・・

Controllerから保存した場合と挙動が違う

これは、テストの挙動と画面から実際に飛んでくるリクエストと比較したらわかった。

そのまんま保存できるのならば、その形式でsaveに渡せば保存はできるはず!
と、テストに以下の記述をしてやってみた。

debug("こうやれば保存できそう。");
$postData = array(
	'Hoge' => array(
		'ID' => $id,
		'TITLE' => 'ほげほげ編集タイトル',
		'CONTENTS' => 'ほげのコンテンツ編集後'
	),
	'Tag' => array(
		array('TAG_ID' => 'TAG002'),
		array('TAG_ID' => 'TAG003'),
	),
);
$this->assertNotEqual($this->Hoge->save($postData), false, "保存に失敗しちゃった。");
debug($this->Hoge->read(null, $id));

できた!実行するたびidがインクリメントされるけど、期待通り保存できた。

array(
	'Hoge' => array(
		'ID' => '1',
		'TITLE' => 'ほげほげ編集タイトル',
		'CONTENTS' => 'ほげのコンテンツ編集後'
	),
	'Tag' => array(
		(int) 0 => array(
			'TAG_ID' => 'TAG002',
			'TAG_TEXT' => 'たぐたぐ2',
			'HogesTags' => array(
				'ID' => '5',
				'HOGE_ID' => '1',
				'TAG_ID' => 'TAG002'
			)
		),
		(int) 1 => array(
			'TAG_ID' => 'TAG003',
			'TAG_TEXT' => 'たぐたぐ3',
			'HogesTags' => array(
				'ID' => '6',
				'HOGE_ID' => '1',
				'TAG_ID' => 'TAG003'
			)
		)
	)
)

Controllerで受け取るリクエストのデータがこの形式ならば問題ないはず!
と、Controllerの中身をちゃんと書いてlogに出してみると・・・こんな結果になる。

2012-12-02 04:07:48 Debug: Array
(
    [Hoge] => Array
        (
            [ID] => 1
            [TITLE] => aaa
            [CONTENTS] => asasas
        )

    [Tag] => Array
        (
            [Tag] => Array
                (
                    [0] => TAG001
                    [1] => TAG002
                )

        )

)

違うじゃん!これじゃだめじゃん!
だからかー。

というわけで・・・

ドウシヨウ...
(´・ω・`)

紐付けの部分をしっかり実装するしかないかなー。
それとも、何か見逃している?