【Laravel】EloquentのsaveメソッドがINSERTとUPDATEを自動判定する仕組み($existsとハイドレーション)
1. 概要 / 結論
- LaravelのEloquentモデルにおいて、
$model->save()を実行した際にINSERT(新規登録)とUPDATE(更新)のクエリが自動で切り替わる内部メカニズムに関する備忘録。 - 切替の判別は、モデル内部の
$existsプロパティ(boolean) の値によって行われ、インスタンスの生成・取得経路(newやfind()など)に応じた内部ハイドレーション処理によって$existsの初期値が決定される。
2. 課題・背景(なぜ調べたか)
-
$model->save()を呼び出すだけで、Laravelがどのように既存レコードの更新なのか新規挿入なのかを判別しているのか、ソースコードレベルの判定基準を把握したかったため。 -
new User()、User::find()、User::firstOrCreate()などの取得・作成手段ごとに$existsフラグがどのように変化するのか、その全体の流れとコード構造を整理したかったため。
3. 実装・解決方法 / 使い方
1. save() メソッドの分岐ロジック(Model.php)
save() メソッド内では、$this->exists の真偽値を見て呼び出す内部メソッドを分岐させている。
// Illuminate/Database/Eloquent/Model.php
public function save(array $options = [])
{
$query = $this->newModelQuery();
// $exists の状態に応じて処理を分岐
if ($this->exists) {
// 変更がある場合(isDirty() === true)のみ UPDATE クエリを発行
$saved = $this->isDirty() ? $this->performUpdate($query) : true;
} else {
// 新規作成時は INSERT クエリを発行
$saved = $this->performInsert($query);
}
return $saved;
}
protected function performInsert(Builder $query)
{
// ... INSERT 文の発行処理 ...
// ★ INSERT 成功後、内部で $exists を true に変更する
$this->exists = true;
$this->wasRecentlyCreated = true;
return true;
}
2. 作成・取得方法と $exists の挙動深掘り
A. new User() による手動インスタンス化($exists = false)
モデルのプロパティ定義として $exists の初期値は false になっているため、手動で new した直後は常に INSERT 側になる。
// Illuminate/Database/Eloquent/Model.php
public $exists = false; // デフォルト値
public function __construct(array $attributes = [])
{
$this->fill($attributes); // 属性をセットするのみで $exists は false のまま
}
B. User::find() や User::get() によるDB取得($exists = true)
DBからデータを取得する際、Builder::hydrate() から Model::newFromBuilder() が呼ばれ、内部で newInstance([], true) が実行されることで最初から $exists = true の状態でインスタンス化される。
// Illuminate/Database/Eloquent/Builder.php
public function hydrate(array $items)
{
$instance = $this->getModel();
return $instance->newCollection(array_map(function ($item) use ($instance) {
return $instance->newFromBuilder($item); // 生データをモデルオブジェクト化
}, $items));
}
// Illuminate/Database/Eloquent/Model.php
public function newFromBuilder($attributes = [], $connection = null)
{
// 第2引数の true が $exists プロパティにセットされる
$model = $this->newInstance([], true);
// DB取得の値を $attributes(現在値)と $original(初期値)の両方に保持(isDirty用)
$model->setRawAttributes((array) $attributes, true);
return $model;
}
public function newInstance($attributes = [], $exists = false, $connection = null)
{
$model = new static((array) $attributes);
$model->exists = $exists; // ★ ここで true がセットされる
return $model;
}
C. firstOrNew() と firstOrCreate() の挙動差
// Illuminate/Database/Eloquent/Builder.php
// firstOrNew: DBに無ければ newModelInstance() で $exists = false のインスタンスを返す
public function firstOrNew(array $attributes = [], array $values = [])
{
if (! is_null($instance = $this->where($attributes)->first())) {
return $instance; // DBにあれば $exists = true
}
return $this->newModelInstance(array_merge($attributes, $values)); // DBになければ $exists = false
}
// firstOrCreate: 無い場合は new したあとに save() を呼んで INSERT 発行 ($exists = true に昇格)
public function firstOrCreate(array $attributes = [], array $values = [])
{
if (! is_null($instance = $this->where($attributes)->first())) {
return $instance;
}
return tap($this->newModelInstance(array_merge($attributes, $values)), function ($instance) {
$instance->save(); // save() 内で performInsert() が動き、$exists = true に変わる
});
}
作成・取得方法ごとの $exists まとめ
| 作成・取得ロジック | 主な内部メソッド |
$exists 初期値 |
save() 実行時の動作 |
|---|---|---|---|
new User() |
__construct() |
false |
INSERT(実行後 $exists = true へ変化) |
User::find(1) / User::get()
|
newFromBuilder() -> newInstance([], true)
|
true |
UPDATE |
User::firstOrNew() |
DBヒット時 newFromBuilder() / 未ヒット時 newModelInstance()
|
DBあり: true DBなし: false
|
DBあり: UPDATE DBなし: INSERT
|
User::firstOrCreate() |
DB未ヒット時に save() まで自動実行 |
取得完了時点では常に true
|
次回から UPDATE
|
4. はまりポイント・注意点
-
setRawAttributes(..., true)と変更検知(isDirty):
DBからの取得時はfill()ではなくsetRawAttributes()が使われる。これにより$attributes(現在の値)と$original(取得時の値)の双方に同じデータが入るため、プロパティ変更時に$this->isDirty()が正しく差分を検知できるようになり、変更がない場合の無駄なUPDATESQL発行を防いでいる。 -
同一インスタンスでの複数回
save():
new User()で生成したインスタンスであっても、1度目の$user->save()が成功した時点で内部で$exists = trueへ自動的に書き換わる。そのため、同じ変数で連続して属性を変更して再度$user->save()を呼ぶと、2回目はUPDATEになる。 -
SoftDeletes(論理削除)との違い:
論理削除機能(SoftDeletes)は$model->delete()実行時にperformDeleteOnModel()をオーバーライドしてUPDATE文(deleted_atの更新)に差し替える仕組みであり、save()のINSERT/UPDATE切り替えロジックとは独立している。