LaravelでクエリビルダーやEloquentを使ったupdateの戻り値の違い

背景

Laravelアプリケーションでレコードをupdateするやり方が色々あり、戻り値の違いについて調べた。

結論

表にまとめた。

書き方 戻り値の型 更新対象がない時の戻り値
\DB::table(‘samples’)->where($where)->update($values) int(件数) 0
\App\Sample::where($where)->update($values) int(件数) 0
\App\Sample::where($where)->first()->update($values) bool(成否) 実行不可
\App\Sample::find($id)->update($values) bool(成否) 実行不可

詳細

それぞれのupdate呼び出し結果をvar_dumpしてみる。

<?php
// 初期データ
\DB::table('samples')->insert([ 'id' => 1, 'name' => 'alice' ]);

echo PHP_EOL;

echo "レコードと差分がない値で更新する場合" . PHP_EOL;
$exists_where = [ 'id' => 1 ];
$same_values  = [ 'name' => 'alice' ];

// クエリビルダーで更新(変更なし)
var_dump(\DB::table('samples')->where($exists_where)->update($same_values));

// Modelのwhere->updateで更新(変更なし)
var_dump(\App\Sample::where($exists_where)->update($same_values));

// Modelのwhere->first->updateで更新(変更なし) -> bool(true)
var_dump(\App\Sample::where($exists_where)->first()->update($same_values));

// Modelのfind->updateで更新(変更なし) -> bool(true)
var_dump(\App\Sample::find($exists_where['id'])->update($same_values));

echo "レコードが存在しない場合" . PHP_EOL;
$not_exists_where = [ 'id' => 999 ];

// クエリビルダーで更新(変更なし)
var_dump(\DB::table('samples')->where($not_exists_where)->update($same_values));

// Modelのwhere->updateで更新(変更なし)
var_dump(\App\Sample::where($not_exists_where)->update($same_values));

// Modelのfirstおよびfindはnullが返るので実行不可

実行すると冒頭の表の通り、int(件数)を返すものとbool(成否)を返すものがある。
何故違うかは、それぞれの実行クラスを見ると分かる。

<?php
var_dump(get_class(\DB::table('samples')->where($exists_where)));
// => string(33) "Illuminate\Database\Query\Builder"

var_dump(get_class(\App\Sample::where($exists_where)));
// => string(36) "Illuminate\Database\Eloquent\Builder"

var_dump(get_class(\App\Sample::where($exists_where)->first()));
// => string(10) "App\Sample"

var_dump(get_class(\App\Sample::find($exists_where['id'])));
// => string(10) "App\Sample"

つまりDB::table経由だろうとモデル経由だろうと、firstなりfindなりで一旦モデルを取得したかどうかで処理が異なる。
なおIlluminate\Database\Eloquent\Builderは内部でIlluminate\Database\Query\Builderに委譲している。

  • 余談1: 試していないが\App\Sample::where($where)->update($values)の場合は各種フックが走らないはず
  • 余談2: \DB::table('samples')->where($where)->first()はモデルではなくstdClassが返る

Illuminate\Database\Query\Builder

このクラスのupdateは最終的にPDOStatement::rowCountを返す。
なので常に更新件数が返る(はず)。

<?php
    /**
     * Run an SQL statement and get the number of rows affected.
     *
     * @param  string  $query
     * @param  array   $bindings
     * @return int
     */
    public function affectingStatement($query, $bindings = [])
    {
        return $this->run($query, $bindings, function ($query, $bindings) {
            if ($this->pretending()) {
                return 0;
            }

            // For update or delete statements, we want to get the number of rows affected
            // by the statement and return that back to the developer. We'll first need
            // to execute the statement and then we'll use PDO to fetch the affected.
            $statement = $this->getPdo()->prepare($query);

            $this->bindValues($statement, $this->prepareBindings($bindings));

            $statement->execute();

            return $statement->rowCount();
        });
    }

Illuminate\Database\Eloquent\Model

App\Sampleが継承しているModelのupdateは以下のようになっている。

<?php
    /**
     * Update the model in the database.
     *
     * @param  array  $attributes
     * @param  array  $options
     * @return bool
     */
    public function update(array $attributes = [], array $options = [])
    {
        if (! $this->exists) {
            return false;
        }

        return $this->fill($attributes)->save($options);
    }

saveはEloquentのフックの処理を色々したりするが、最終的には必ずboolを返す。
件数を返すことはないように見える。

なお$this->existsで見ているexistsはレコードをチェックしているわけではなく、論理上のフラグである。
モデルのコンストラクタで指定するか、insertに成功したりするとフラグが立つ。