cakephp で 履歴テーブル

テーブルの変更内容全てを履歴として残して欲しい。
こういった要望はしばしば見受けられるのでメモ。
基本的にcakePHPの命名規則にのっとって記述する。

cakePHP 2.3.10

DBの準備

例として users テーブルとその履歴テーブル his_users を用意する。
(履歴テーブルは必ず his_ を冠するルールにする。)
CREATE文はこちら。

CREATE TABLE `users` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `created` datetime NOT NULL,
 `modified` datetime NOT NULL,
 `delete_flag` tinyint(4) NOT NULL,
 `name` text NOT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

CREATE TABLE `his_users` (
 `his_id` int(11) NOT NULL AUTO_INCREMENT,
 `id` int(11) NOT NULL,
 `created` datetime NOT NULL,
 `modified` datetime NOT NULL,
 `delete_flag` tinyint(4) NOT NULL,
 `name` text NOT NULL,
 PRIMARY KEY (`his_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

フィールドの構造は基本的に揃える。
ただし,本テーブルである users は主キーとして id を用いるが,
履歴テーブルである his_users は主キーを his_id とし,id にはインデックスはもたせない。

AppModelの準備

save()を使った保存方法は大きく2通り。

(1) save()の引数を利用する方法

$Model->save($data);

(2) set()を使用する方法

$Model->set($data);
$Model->save();

ちなみに set($data1) した後に save($data2) した場合 $data2 が上書きされる。
$Model->set($hoge) を利用すると $Model->data が $hoge を保存しており,
$hoge に id の記述があると自動で $Model->id に idが入る。
$Model->save(); 実行後, $Model->data は消去されるが $Model->id は消去されない。
レコード新規追加の場合は $Model->id に主キーが代入されている。

これらの性質を利用して以下のようにオーバーライドする。

public function save($data = null, $validate = true, $fieldList = array(), $his_flag = true) {
	
	// セーブデータ取得
	if ($data !== null) {
		$this->set($data);
	}
	
	// セーブ実行
	if ($r = parent::save($data, $validate, $fieldList) && $his_flag) {
	
		// セーブされたデータを取得
		$_data = $this->find('first', array(
			'conditions' => array(
				$this->name . '.id' => $this->id
			)
		));
		
		// 履歴テーブルのモデルロード
		$his_name = 'His' . $this->name;
		if ($this->$his_name = @Classregistry::init($his_name)) {
		
			// 履歴テーブルへの保存
			$this->$his_name->create();
			$this->$his_name->primaryKey = 'his_id';
			$_data[$his_name] = $_data[$this->name];
			unset($_data[$this->name]);
			$this->$his_name->save($_data, $validate, $fieldList, false);
			
		}
		
	}
	
	// 終了
	return $r;
}

まとめ・気づき

(1) save() をオーバーロードしたら saveMay() などにも適用できた。
(2) HisUser.php などは作成しなくても動く。(後から考えたら当たり前だった。)