はじめに
開発中に面白い現象に遭遇したので残しておく。
Eloquentでレコードを1件だけ削除したはずが、複数件削除されてしまった問題が発生して非常に困った。
この記事では、Eloquentの内部実装を追いながら解説する。
問題のコード
以下は、nameで絞り込んだレコードを1件取得して削除するコード。
$deleteTarget = $model->where('name', $name)->first();
$deleteTarget->delete();
どう見ても1件のレコードを削除するだけのコードだが、実際に実行すると複数のレコードが削除された。
なぜこんな現象が発生するのか。
原因の調査
Eloquentのdelete()メソッドの内部実装を調べてみた。
Laravel 10のEloquentでは、first()で取得したモデルインスタンスのdelete()は、インスタンス取得時のwhere条件を使わず、インスタンスが持つプライマリーキー値のみを使って削除処理を実行する。
つまり、where('name', $name)で絞り込んでも、実際の削除処理ではWHERE プライマリーキー = 値という条件に置き換わる。
ここでDBの設計が怪しいことが分かる。
// Laravel 10.x の Illuminate\Database\Eloquent\Model より抜粋
// https://github.com/laravel/framework/blob/10.x/src/Illuminate/Database/Eloquent/Model.php
namespace Illuminate\Database\Eloquent;
abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToString, HasBroadcastChannel, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable
public function delete()
{
// 省略
$this->performDeleteOnModel();
return true;
}
protected function performDeleteOnModel()
{
$this->setKeysForSaveQuery($this->newModelQuery())->delete();
$this->exists = false;
}
protected function setKeysForSaveQuery($query)
{
$query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery());
return $query;
}
public function getKeyName()
{
return $this->primaryKey;
}
問題の根本原因
テーブルを確認すると原因が見えてきた。
状況の整理:
- データベースのテーブルにはプライマリーキー制約が設定されていない
- しかし、Eloquentモデルでは
idカラムをプライマリーキーとして指定している - 実際のテーブルでは、この
idカラムに重複する値が存在している
テーブルにプライマリーキー制約がなく、Eloquentモデルで指定しているidカラムに重複値が存在する場合、WHERE id = 値という条件で複数レコードがヒットしてしまう。
結果として、delete()を実行すると、そのid値を持つすべてのレコードが削除される。
以上のようなテーブル構造とモデル設定の不整合が、今回の予期しない動作の原因だった。
応急対処法
今回は一時的な対処として、以下のようにクエリビルダーを使って明示的に条件を指定する方法で対応した。
Model::query()->where('id', $id)->where('name', $name)->delete();
この方法では、Eloquentのモデルインスタンスを通さずに、指定した条件に合致するレコードのみを削除できるため、想定通りの動作になる。
類似の問題
この問題はdelete()だけでなく、update()やsave()などの他のCRUD操作でも同様に発生する可能性がある。
いずれもEloquentがモデルインスタンスのプライマリーキーを基準として処理を行うため。
特に注意が必要な環境:
- データベースとアプリケーションが1対多の関係(複数のチームやシステムが同じDBを使用)
- モデルの設定がテーブル構造を正しく反映していない環境
- チーム開発でのモデル設定の反映漏れが発生しやすい環境
根本的な解決策
1. テーブル構造の修正(推奨)
可能であれば、テーブルに適切なプライマリーキー制約を設定する
2. 明示的なクエリの使用
重要な処理では、クエリビルダーを使って明示的に条件を指定する
まとめ
ORMは便利だけど、特徴を理解して設計、実装する必要がある。
重要なポイント:
- フレームワークの自動的な処理を過信せず、動作原理を理解する
- データベース設計とモデル設定の整合性を保つ
- 重要な処理では必ずテスト環境で動作確認を行う
- 必要に応じてソースコードを読み、内部実装を理解する