やっとソースを読んで見つけてたのでメモ。
やりたいこと
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 | ★この値で排他 |
最初に、正解です。
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クエリ生成箇所をみた
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処理部分は
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);
public function update(array $values)
{
return $this->toBase()->update($this->addUpdatedAtColumn($values));
}
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)
));
}
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をセットしてやるように自前でオーバーライドする必要あり。
とりあえずここまで。