CakePHPのsaveでINSERT IGNOREを(ちょい無理やり)使えるようにする

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
時には無視が必要なこともある……

この記事を三行にまとめると

INSERT IGNOREをCakePHPで使えるようにしたい
saveIgnoreというメソッドを自作して対応してみる
使う時は注意が必要ですね
MySQLのINSERT IGNOREをCakePHPで使えるようにしたいというお話です。CakePHP2系での話になるんでちょい古いバージョンになってしまいますが、よかったらおつき合いくだせえ。


INSERT IGNOREについて

まずはざっくりとINSERT IGNOREとは何ぞやという点について。

MySQLで新規にデータを登録する際にはINSERTを使いますよね。この時、すでに存在するユニークなデータをINSERTしようとすると「Duplicate entry 何ちゃら for かんちゃら」みたいなエラーが出てしまいます。

ようはユニークなデータは重複して登録できねーよってことなんですけど、このエラーを回避する方法としてINSERT文にIGNOREをつけるというやり方があります。

例えば今こんなデータがテーブルに入ってるとする。

+----+---------+-------------+
| id | user_id |  product_id |
+----+---------+-------------+
|  1 |    1    |       1     |
|  2 |    1    |       2     |
|  3 |    2    |       1     |
+----+---------+-------------+

UNIQUE KEY `user_product` (`user_id`,`product_id`),

すごい適当ですがここではユーザーIDとプロダクトIDの組み合わせがユニークであるとします。

この時、ユーザーIDが1でプロダクトIDが1のデータを新たにINSERTしようとすると例のDuplicate何とかエラーが出ます。

// SQL
INSERT INTO table (user_id, product_id) VALUES (1, 1)

// 実行結果
Duplicate entry '1-1' for key 'user_product'

こんな感じですね。

でもIGNOREをつけるとDuplicateエラーが出なくなります。

// SQL
INSERT IGNORE INTO table (user_id, product_id) VALUES (1, 1)

// 実行結果
Query OK, 1 row affected

IGNOREというのは文字通りに無視するという意味なのですが、これをつけることでエラーが無視されるようになります。あくまでもエラーを無視するだけなのでデータは登録されません。

つまり上記のようなパターンの場合、INSERT IGNOREはエラーがなければ通常通りにデータを登録し、エラーがあれば何もせず、エラーも出さずに処理を終えるということですね。



saveメソッドでINSERT IGNORE

CakePHPのsaveメソッドはデータがなければINSERT、データが存在すればUPDATEに自動で切り替えてくれる良い奴ですが、場合によってはこれが上手く機能せずにデータがあるのにINSERTを実行しようとしてしまうことがあるかもしれません。そんな時はINSERT IGNOREでエラーを回避したいと思うかもしれませんが、デフォルトのsaveメソッドにはINSERT IGNOREを実行する機能が備わっていません。だからsaveメソッドを書き換えるか自分でIGNOREを実行できるメソッドを作る必要があります。

今回はsaveIgnoreというメソッドを自作して対応してみることにします。

まずはAppModel.phpにsaveIgnoreというメソッドを作ります。中身はとりあえずsaveメソッドをまるっとコピぺします。saveメソッドの中身は結構ボリューミーなのでここでは省略します。

// AppModel.php
public function saveIgnore($data = null, $validate = true, $fieldList = array()) {
  // saveメソッドのコードを丸コピ
}

んでここから必要な部分の修正を行いますが、saveメソッドの後半の方にこんなコードがあります。

if(!$db->create($this, $fields, $values)) {
  $success = false;
}

この「$db->create」はDboSource.phpというファイルの中にあるcreateメソッドを呼び出しています。なのでここを「createIgnore」というメソッドを呼び出すように書き換えます。

if(!$db->createIgnore($this, $fields, $values)) {
  $success = false;
}

引数とかはそのままで良いです。これでcreateIgnoreを呼び出せるようになりました。



次にDboSource.phpにcreateIgnoreメソッドを自作します。DboSource.phpはModelフォルダのDatasourceというフォルダの中にあるので、それをapp側にコピーしてcreateIgnoreメソッドを追加します。

// DboSource.php
public function createIgnore(Model $model, $fields = null, $values = null) {
  // createメソッドのコードを丸コピ
}

中身は既存のcreateメソッドをコピペすればOKです。これでひとまずは通常のcreateと同じ動きになります。

createメソッドの中にはこんなコードがあります。

if($this->execute($this->renderStatement('create', $query))) {
  // 中略
}

renderStatementというメソッドを呼び出しています。このrenderStatementもDboSource.phpの中にあるメソッドなのですが、このrenderStatementがSELECT文やらINSERT文やらUPDATE文やらDELETE文を作成しているメソッドになります。

実際にrenderStatementメソッドを見るとこんなコードが書いてあります。

switch(strtolower($type)) {
  case 'select':
    return "SELECT {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group} {$order} {$limit}";
  case 'create':
    return "INSERT INTO {$table} ({$fields}) VALUES ({$values})";
  
  // 中略
}

renderStatementを呼び出す際に第一引数にcreateを指定すればINSERT文を発行してくれることが分かりますね。

なのでこのメソッドにINSERT IGNOREを発行してくれるタイプを追加すればINSERT IGNOREが使えるようになります。

switch(strtolower($type)) {
  case 'select':
    return "SELECT {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group} {$order} {$limit}";
  case 'create':
    return "INSERT INTO {$table} ({$fields}) VALUES ({$values})";
  case 'insert_ignore':
    return "INSERT IGNORE INTO {$table} ({$fields}) VALUES ({$values})";
  
  // 中略
}

こんな感じですね。通常のINSERTとIGNOREの違いはINSERTの後ろにIGNOREがあるかどうかだけなので、他の部分はcreateの下にあるINSERT文をそのままコピペすればOKです。

これでrenderStatementがINSERT IGNOREを発行してくれるようになったので、先ほど自作したcreateIgnoreでこいつを呼び出すようにします。

if($this->execute($this->renderStatement('insert_ignore', $query))) {
  // 中略
}

あとはデータを登録する際にsaveメソッドではなくsaveIgnoreメソッドを呼び出せばINSERT IGNOREを実行してくれます。

$data = array('user_id' => 1, 'product_id' => 1);
$model->create($data);
$model->saveIgnore();






結構無理やりな気もしますが、一応これでINSERT IGNOREを使えるようになります。もちろんqueryメソッドに直接INSERT IGNOREを書いても実行はできますが、個人的には直接SQL文を書いてqueryメソッドを呼び出すのは最後の手段だと思っているので極力やらないようにしています。

INSERT IGNOREに関しても本来エラーが出るはずのところでエラーが出ないのは決して安全とは言えないので、どうしてもって時の苦肉の策として使うくらいにとどめておくのが無難かなと思います。

あとIGNOREはあくまでもINSERT時のエラーを無視する機能であって重複データのエラーをなくすための機能ではないので、場合によっては登録されたら困るデータがエラーを無視して登録できてしまったりする危険性もあります。使う時は注意が必要ですね。
 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください