0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Laravel】EloquentのsaveメソッドがINSERTとUPDATEを自動判定する仕組み($existsとハイドレーション)

0
Posted at

【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() が正しく差分を検知できるようになり、変更がない場合の無駄な UPDATE SQL発行を防いでいる。
  • 同一インスタンスでの複数回 save():
    new User() で生成したインスタンスであっても、1度目の $user->save() が成功した時点で内部で $exists = true へ自動的に書き換わる。そのため、同じ変数で連続して属性を変更して再度 $user->save() を呼ぶと、2回目は UPDATE になる。
  • SoftDeletes(論理削除)との違い:
    論理削除機能(SoftDeletes)は $model->delete() 実行時に performDeleteOnModel() をオーバーライドして UPDATE 文(deleted_at の更新)に差し替える仕組みであり、save() の INSERT/UPDATE 切り替えロジックとは独立している。

5. 参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?