日記

日々のことと、Python/Django/PHP/Laravel/nodejs などソフトウェア開発のことを書き綴ります

Laravel 5.1 入門記 その12 (Eloquent 論理削除編)

Eloquent がサポートしている論理削除 (Soft Deletes) についてです。 今回も教材はこちら。

Eloquent: Getting Started - Laravel - The PHP Framework For Web Artisans

Soft Deletes

早速。

Laravel は論理削除をサポートする仕組みを標準で用意してます。論理削除カラムを持つテーブルに対応したモデルを書くときは以下の様になります。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Flight extends Model
{
    use SoftDeletes;

    /**
     * The attributes that should be mutated to dates.
     *
     * @var array
     */
    protected $dates = ['deleted_at'];
}

こんな感じです。論理削除対応をしたテーブルは deleted_at というカラムが必要です。 ちなみに論理削除のカラムは、「timestamp」です。数値フィールドではありません。

論理削除対応のクエリ

論理削除対応のモデルを定義して、以下の様にデータを取得すると

$flights = App\Flight::all();

実行される SQL

select * from `flights` where deleted_at is null;

こんな感じで、自動的に deleted_at is null が出力されます。論理削除されたデータも取りたいときや無差別にデータを取りたいときにも対応していて

$flights = App\Flight::withTrashed()->get();

$flights = App\Flight::onlyTrashed()->get();

こんな書き方で取得できます。 withTrashed が論理削除済みも含む全てで、onlyTrashed が論理削除済みのデータのみです。

論理削除の方法

論理削除を実行する方法は、通常のモデルで行った方法がそのままです。

App\Flight::where('active', 1)->delete();

App\Flight::destroy([1,2,3]);

などなど。ただし、 SoftDeletes の trait を継承した段階で、このやり方では物理削除が出来なくなります。どうしても物理削除したい場合は

App\Flight::where('active', 1)->forceDelete();

と、forceDelete を使います。

論理削除を boolean / integer など数値カラムにする方法

レガシーなデータ構造だと、削除「フラグ」っていうことが多いですよね! ね!!!

Timestamp から数値系のカラムに SoftDeletes の仕組みを置き換えるときは、Illuminate\Database\Eloquent\SoftDeletes の trait で定義されている以下のメソッドをオーバーライドして置き換えをします。

  • bootSoftDeletes
  • forceDelete
  • performDeleteOnModel
  • runSoftDelete
  • restore
  • trashed
  • withTrashed
  • onlyTrashed
  • restoring
  • restored
  • getDeletedAtColumn
  • getQualifiedDeletedAtColumn

いっぱいあります。ポイントとしては Eloquent のクラスが SoftDeletes に依存する形で実装が行われているため、自前のクラスに以下の様な定義が必要です。

use Illuminate\Database\Eloquent\SoftDeletes;

class BaseModel extends Model
{
    use SoftDeletes;

ちゃんと use SoftDeletes を書いた上でオーバーライドが必要。この辺り、本当にもうちょっと何とかならなかったのかと思う。

更に、Illuminate\Database\Eloquent\SoftDeletingScope を継承してメソッドをオーバーライドしていく必要もあります。こちらは上記のモデルクラスから bootSoftDeletes などからインスタンスを作って使用するだけなので、変な癖は少ないです。が、一箇所だけ remove メソッドで非常に実装に困ったところがあったので、そこだけ載せておきます。

    public function remove(Builder $builder, Model $model)
    {
        $column = $model->getQualifiedDeletedAtColumn();

        $query = $builder->getQuery();
        $bindings = $builder->getBindings();
        $count = 0;

        $query->wheres = collect($query->wheres)->reject(function ($where) use ($column, &$count, &$bindings) {
            $result = $this->isSoftDeleteConstraint($where, $column);
            if ($result) {
                unset($bindings[$count]);
            }
            $count++;
            return $result;
        })->values()->all();
        $query->setBindings(array_merge($bindings));
    }

追加した論理削除を除外する条件を除去する処理で、 QueryBuilder が持っている条件から条件式の除去と、パラメータを除去する処理を行っています。 元々の SoftDeletes は is null / is not null で論理削除判断をしていたので、 Closure を使って条件を除去する際に $count などの処理は不要で、$query->setBindings の辺りも不要です。しかし、数値型のパラメータで論理削除判断を行う時は、パラメータも使用しないといけないので、こんな面倒な処理が必要になってしまいます。

withTrashed を使った時にこの処理が呼ばれるんですが、最初は気が付かなくて論理削除のパラメータが残ってしまい、結果クエリが正常に実行できないとかで、迷惑を掛けてしまった。

更に使う機会が少ないかもしれませんが、 Laravel の SoftDeletes はリレーションを定義するクラス HasManyThrough も依存してます。結局、自分がプロダクションで使った時は、ここまでの対応はやりませんでしたが…。

  • Illuminate\Database\Eloquent\Relations\HasManyThrough を継承してカスタマイズ
  • Illuminate\Database\Eloquent\Model の hasManyThrough メソッドをオーバーライドして上記の HasManyThrough をカスタムしたクラスの利用

などが必要になってくると思います。 果たして、ここまでして SoftDeletes の仕組みに載っかることが重要なのか分かりませんが…。

話が脱線しましたが、みんな大好き論理削除編は終了。 ちょうど、論理削除なんてない!ドメインに集中しろ!と本日の TL は盛り上がっているところではありますが…。

続いて次回は、Laravel のリレーション編です。