【Laravel】複合キーを使用している場合に、Eloquentが2回DELETEを発行する問題と対策
はじめに
複合主キーを設定しているモデルに対して、whereで条件を指定しdelete()で削除処理を行った際に、意図しない DELETE クエリが追加で発行されてしまう現象に遭遇しました。
原因の特定と解決に時間を要したため、備忘録として記録に残します。
現象
複合主キーを持つテーブルに対して、order_noとitem_noをwhere条件で指定し、delete()を実行しました。
$model = MstDevice::where('order_no', $order_no)
    ->where('item_no', $item_no)
    ->first();
if ($model) {
    $model->delete();
}
発生した現象
- 
1つ目の DELETE クエリ(意図した削除)
WHERE order_no = ? AND item_no = ? - 
2つ目の DELETE クエリ(意図しない削除)
WHERE order_no = ? 
order_noのみを条件にした DELETE クエリが追加で発行され、意図しない範囲のレコードが削除されてしまいました。
Eloquentが意図しないDELETEを発行する理由
1. Eloquentは複合主キーをサポートしていない
Laravel の Eloquent は単一の主キーを前提に設計されており、$primaryKeyに複数のカラムを指定できません。
このため、Eloquent のdelete()はモデルの主キーのみを条件に削除を行います。
protected $primaryKey = ['order_no', 'item_no']; // Laravel標準では無効
この設定は Laravel では無効なため、Eloquent はorder_noだけを主キーとして認識し、item_noを無視する可能性があります。
 2. delete()の内部動作
Eloquent のdelete()は、以下の手順で削除処理を行います。
- 
whereで条件を指定し、first()でモデルを取得する。 - 取得したモデルの
primaryKeyを元に DELETE クエリを発行する。 - 
$primaryKeyがorder_noのみと認識されるため、WHERE order_no = ?だけで削除される。 
この仕様により、意図しないレコード削除が発生します。
対策
 1. 明示的にwhere条件を指定して削除する
delete()をモデルのインスタンスで実行せず、クエリビルダで直接削除を行います。
MstDevice::where('order_no', $order_no)
    ->where('item_no', $item_no)
    ->delete();
この方法であれば、意図しない DELETE クエリが発行されることはありません。
2. getKeyName() の設定を適切に行う(非推奨)
一応、以下のようにgetKeyName()をオーバーライドすれば、複合主キーのように扱うことも可能です。
public function getKeyName()
{
    return ['order_no', 'item_no'];
}
※ただし、Eloquent は複合キーを標準サポートしていないため、別の予期しない挙動を引き起こす可能性があります。whereを明示的に指定する方法の方が安全です。
まとめ
| 方法 | 説明 | 推奨度 | 
|---|---|---|
$model->delete() | 
primaryKeyが単一キーと誤認される | 
× | 
Model::destroy() | 
primaryKeyが単一キーと誤認される | 
× | 
Model::find() | 
primaryKeyが単一キーと誤認される | 
× | 
Model::where()->delete() | 
安全に削除可能 | ○ | 
getKeyName() を複合キーにする | 
Laravel標準では非推奨 | △ | 
推奨される削除方法
MstDevice::where('order_no', $order_no)
    ->where('item_no', $item_no)
    ->delete();
Laravel では複合主キーをサポートしていないため、明示的にwhere条件を指定して削除するのが安全です。
Eloquent の仕様を理解し、意図しないレコード削除を防ぎましょう!
Discussion