Help us understand the problem. What is going on with this article?

Laravel5.2のEloquentで、生のSQLを実行させる

More than 1 year has passed since last update.

やっとソースを読んで見つけてたのでメモ。

やりたいこと

Eloquentのupdate時に、
SET colA = colA+1 
のような、生のSQLを入れ込みたかった。

楽観排他を実装したくて、& updated_at(timestamp)でなくて、version_noで管理したかった
(以前まで使っていた別言語のORMの文化って言われれればそれまでなんだけど・・・)

やったこと

各モデルの親クラスとして、こんな独自クラスを用意。
各テーブルの共通カラムとして以下を付加するようにします。

field type
id int pk
created_at timestamp Eloquent規定
updated_at timestamp Eloquent規定
create_user_id int
update_user_id int
version_no int ★この値で排他

最初に、正解です。

Mymodel.php
class MyModel extends Model {


    protected static function boot() {
        parent::boot();

        self::creating(function($model) {
            return $model->onCreatingHandler();
        });

        self::updating(function($model) {
            return $model->onUpdatingHander();
        });



        // memo:softDeleteの排他項目updateについては、SoftDeletesをextendしたMySoftDelete traitで。
//
//      self::deleted(function ($model) {
//          return $model->onDeletingHander();
//      });

    }


    /**
     * create時のハンドラ
     * @return bool
     */
    protected function onCreatingHandler() {
        $user = Auth::user();

        // 登録、更新IDを入れる
        if (isset($user)) {
            $this->create_user_id = $user->id;
            $this->update_user_id = $user->id;
        } else {
            $this->create_user_id = 0;
            $this->update_user_id = 0;
        }
        $this->version_no = 1;
        return true;
    }

    /**
     * update時のハンドラ
     * @return bool
     */
    protected function onUpdatingHander() {

        $user = Auth::user();
        // 更新ユーザIDの登録
        if (isset($user)) {
            $this->update_user_id = $user->id;
        } else {
            $this->update_user_id = 0;
        }
        // 排他用
        $this->version_no = new Illuminate\Database\Query\Expression('version_no + 1');

        return true;
    }
}

試行錯誤の記録

試行錯誤の記録1:modelに足してみる。

protected function onUpdatingHander() {

    $user = Auth::->user();
    // 更新ユーザIDの登録
    if (isset($user)) {
        $this->update_user_id = $user->id;
    } else {
        $this->update_user_id = 0;
    }
    // 楽観排他
    $this->version_no = $this->version_no + 1 ;

    return true;
}

結果:
staticな状態でfillしてupdateしたら・・おわかりですね。
$this->version_no が0なので、何度やっても1です。失敗。

試行錯誤の記録2:書いてみる

protected function onUpdatingHander() {

    $user = Auth::->user();
    // 更新ユーザIDの登録
    if (isset($user)) {
        $this->update_user_id = $user->id;
    } else {
        $this->update_user_id = 0;
    }
    // 楽観排他
    $this->version_no =  'version_no + 1' ;

    return true;
}

結果:
当たり前ですが、stringとして評価されて?で置換されたので、常に0です。

試行錯誤の記録3:コードリーディングしてupdateクエリ生成箇所をみた

Model.php
public function save(array $options = [])
{
    $query = $this->newQueryWithoutScopes();

    // If the "saving" event returns false we'll bail out of the save and return
    // false, indicating that the save failed. This provides a chance for any
    // listeners to cancel save operations if validations fail or whatever.
    if ($this->fireModelEvent('saving') === false) {
        return false;
    }

    // If the model already exists in the database we can just update our record
    // that is already in this database using the current IDs in this "where"
    // clause to only update this model. Otherwise, we'll just insert them.
    if ($this->exists) {
        $saved = $this->performUpdate($query, $options);
    }

    // If the model is brand new, we'll insert it into our database and set the
    // ID attribute on the model to the value of the newly inserted row's ID
    // which is typically an auto-increment value managed by the database.
    else {
        $saved = $this->performInsert($query, $options);
    }

    if ($saved) {
        $this->finishSave($options);
    }

    return $saved;
}

createにしろupdateにしろ、このsave()メソッドを通り、実際のupdate処理部分は

Model.php
protected function performUpdate(Builder $query, array $options = [])
{
    $dirty = $this->getDirty();

    if (count($dirty) > 0) {
        // If the updating event returns false, we will cancel the update operation so
        // developers can hook Validation systems into their models and cancel this
        // operation if the model does not pass validation. Otherwise, we update.
        if ($this->fireModelEvent('updating') === false) {
            return false;
        }

        // First we need to create a fresh query instance and touch the creation and
        // update timestamp on the model which are maintained by us for developer
        // convenience. Then we will just continue saving the model instances.
        if ($this->timestamps && Arr::get($options, 'timestamps', true)) {
            $this->updateTimestamps();
        }

        // Once we have run the update operation, we will fire the "updated" event for
        // this model instance. This will allow developers to hook into these after
        // models are updated, giving them a chance to do any special processing.
        $dirty = $this->getDirty();

        if (count($dirty) > 0) {
            $numRows = $this->setKeysForSaveQuery($query)->update($dirty);

            $this->fireModelEvent('updated', false);
        }
    }

    return true;
}


ここで、

さっき"updating"イベントのクロージャに設定したメソッドを実行

Eloquent規定のupdated_atカラムの設定

update実行

updatedイベントの実行(今回は設定なし)

となるわけで、実際にupdate文を組み立てているところを探す。
$numRows = $this->setKeysForSaveQuery($query)->update($dirty);

Illuminate\Database\Eloquent\Builder.php
public function update(array $values)
{
    return $this->toBase()->update($this->addUpdatedAtColumn($values));
}
Illuminate\Database\Query\Builder.php
public function update(array $values)
{
    $bindings = array_values(array_merge($values, $this->getBindings()));

    $sql = $this->grammar->compileUpdate($this, $values);

    return $this->connection->update($sql, $this->cleanBindings(
        $this->grammar->prepareBindingsForUpdate($bindings, $values)
    ));
}
Illuminate\Database\Query\Grammars\Grammar.php
public function compileUpdate(Builder $query, $values)
{
    $table = $this->wrapTable($query->from);

    // Each one of the columns in the update statements needs to be wrapped in the
    // keyword identifiers, also a place-holder needs to be created for each of
    // the values in the list of bindings so we can make the sets statements.
    $columns = [];

    foreach ($values as $key => $value) {
        $columns[] = $this->wrap($key).' = '.$this->parameter($value);
    }

    $columns = implode(', ', $columns);

    // If the query has any "join" clauses, we will setup the joins on the builder
    // and compile them so we can attach them to this update, as update queries
    // can get join statements to attach to other tables when they're needed.
    if (isset($query->joins)) {
        $joins = ' '.$this->compileJoins($query, $query->joins);
    } else {
        $joins = '';
    }

    // Of course, update queries may also be constrained by where clauses so we'll
    // need to compile the where clauses and attach it to the query so only the
    // intended records are updated by the SQL statements we generate to run.
    $where = $this->compileWheres($query);

    return trim("update {$table}{$joins} set $columns $where");
}

ここの、
$columns[] = $this->wrap($key).' = '.$this->parameter($value);
でSET部分のクエリをくみたててる。

public function parameter($value)
{
    return $this->isExpression($value) ? $this->getValue($value) : '?';
}

というわけで、Expression型ならまんまクエリの一部と評価して繋げてくれることがわかりスッキリ!

余談

  • 楽観排他として、本当はupdate時にwhere句にversion_noをいれて結果数で判定したかったんだけど、結果行を返してくれないので、諦めてsharedLock()でselectしている。
  • memoしてあるけど、論理削除=softdeleteの機構を使うとき、このsaveは通らないので、残念ながらSoftDeletesをextendしたtraitを用意して、runSoftDelete()を同様にversion_noをセットしてやるように自前でオーバーライドする必要あり。

とりあえずここまで。

you-me
一番長いのはWEB系Java。他PHP(WordPress/Laravel), .NET, HTML5/CSS/js/jQueryなどちまちま。他Python/Flask/Swift/Unityちょっぴりなど。直近はTypescript/createjsをもりもり(記事書くのおサボり中)
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away