【Laravel】DB操作のrevert(時を戻そう)機能をつくる

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、これは私に限らないかもしれませんが、何気なく過ごしていると「そういえばアレ面白かったな〜」とクスっとしてしまったりします。

そして先日はその影響でLaravelでつくりたいある機能を思いつきました。

その機能とは・・・・・・

revert 機能 

です。

revertとは、簡単に言うとデータをある特定の時点に戻すことができる機能のことで、お笑いコンビ「ぺこぱ」さんが漫才の中で使っていた「時を戻そう!」というフレーズからアイデアが浮かびました😂(漫才面白かったです!)

ということで、今回は「データをいつの状態にでも戻すことができる」revert機能をつくってみたいと思います。

ぜひ皆さんのお役に立てると嬉しいです😊✨

「外出れないと、YouTubeばっか見ちゃいますね😂」

開発環境: Laravel 7.x

やりたいこと

今回の開発で実装する細かな内容は次のとおりです。

  • データが作成、更新、削除されるたびに履歴データを追加していく
  • データが削除されても元に戻せるようにする
  • どのモデルでも汎用的につかえるようにする
  • 管理しやすいようにバージョンをつける

ではやっていきましょう❗

編集履歴を保存するテーブルをつくる

まず履歴データを保存しておく「histories」テーブルとそのモデルをつくります。以下のコマンドを実行してください。

php artisan make:model History -m

すると、モデルとマイグレーションが一気に作成されます。
まずはマイグレーションの設定をします。

database/migrations/****_**_**_******_create_histories_table.php

// 省略

public function up()
{
    Schema::create('histories', function (Blueprint $table) {
        $table->id();
        $table->string('model');
        $table->unsignedBigInteger('model_id');
        $table->integer('version');
        $table->string('type'); // `created`, `updated`, `deleted` or `reverted`
        $table->text('data')->nullable(); // JSON
        $table->timestamps();
    });
}

// 省略

この中で設定しているフィールドは以下のとおりです。

  • model: 対象のモデル
  • model_id: 対象モデル(テーブル)のID
  • version: 履歴バージョン
  • type: どんな操作をしたかが分かる操作タイプ
  • data: 対象データ(JSON化して保存)

では、マイグレーションが完成したら以下のコマンドで実行しておきましょう。

php artisan migrate

テーブルはこのようになります。

続いてモデルの設定です。

app/History.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class History extends Model
{
    protected $casts = [
        'data' => 'json'
    ];

    // Others
    public function revert() {

        $self = $this; // use() には $this を使えないので別の変数へ格納

        Model::withoutEvents(function() use($self){ // この中はイベントが実行されない

            // データを元に戻す
            $model = (new $self->model())->firstOrNew(['id' => $self->model_id]);

            foreach($self->data as $key => $value) {

                $model->{$key} = (is_array($value)) ? json_encode($value) : $value;

            }

            $model->save();

            // 新しい履歴を追加
            $history = $self->replicate(); // 履歴データを複製
            $history->version = $self->getNextVersionNumber($history->model, $history->model_id);
            $history->type = 'reverted';
            $history->save();

        });

    }

    public static function getNextVersionNumber($model, $id) {

        return self::where('model', $model)
                ->where('model_id', $id)
                ->max('version') + 1;

    }
}

この中でやっているのは、まず$castsを使って以下の自動変換ができるようにしています。

  • 文字列 → JSON
  • JSON → 文字列

そして、historiesテーブルに保存されたデータを使って「時を戻そう!」を実行するrevert()メソッドを追加しました。

つまり、実際には

$history->revert();

という使い方をすることになります。

なお、この中で重要なのがModel::withoutEvents()の部分です。
この中で保存されたものは、イベントがあっても無視されることになるのですが、こうしていないと自動的にupdatedのデータが追加されてしまうためです。(実際には代わりにrevertedのデータを追加します)

データ操作をした時に実行されるイベントをつくる

次に、データ操作するたびにhistoriesにデータを追加するのはめんどうなので、自動的にコードを実行してくれる「イベント」をつくっておきましょう。

なお、今回は一気にイベントをつくりやすいObserverを使った方法になります。

履歴用イベントをつくる

以下のコマンドで専用Observerをつくります。

php artisan make:observer HistoryObserver

すると、ファイルが作成されるので中身を以下のように変更します。

app/Observers/HistoryObserver.php

<?php

namespace App\Observers;

use Illuminate\Database\Eloquent\Model;

class HistoryObserver
{
    public function created(Model $model) {

        $this->saveHistory($model, 'created');

    }

    public function updated(Model $model) {

        $this->saveHistory($model, 'updated');

    }

    public function deleted(Model $model) {

        $this->saveHistory($model, 'deleted');

    }

    private function saveHistory($model, $type) {

        $model_class = get_class($model);
        $primary_key = $model->getKeyName();
        $model_id = $model->{$primary_key};
        $version = \App\History::getNextVersionNumber($model_class, $model_id);
        $hidden = $model->getHidden();
        $data = $model->makeVisible($hidden);

        $history = new \App\History;
        $history->model = $model_class;
        $history->model_id = $model_id;
        $history->version = $version;
        $history->type = $type;
        $history->data = $data;
        $history->save();

    }
}

なおイベントとして実行されるのは以下の3つで、saveHistory()はそれぞれのメソッドから呼び出されることになります。

  • created: データが作成された時
  • updated: データが更新された時
  • deleted: データが削除された時

作成したイベントを有効にする

では、先ほど作成したHistoryObserverを有効にしておきましょう。
今回はUserモデルで「時を戻そう!」機能をつかうことにします。

app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;

use App\Observers\HistoryObserver; // 👈 追加
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    // 省略

    public function boot()
    {
        \App\User::observe(HistoryObserver::class); // 👈 追加
    }
}

※つまり、もしItemモデルで使いたい場合は、\App\Item::observe(HistoryObserver::class);となります。

さぁ、これで準備は完了しました❗

テストしてみる

ではusershistoriesテーブルを空にしてテストしてみます。

まずは、ユーザー追加です。

$user = new \App\User;
$user->name = 'すこひ';
$user->email = 'sukohi.1@example.com';
$user->password = bcrypt('secret');
$user->save();

これを実行すると、テーブルは以下のように1件ずつデータ登録されました。(historiesのデータは、createdタイプとして登録されています)

続いて、このユーザーのメールアドレスを更新してみましょう。

$user = \App\User::find(1);
$user->email = 'sukohi.2@example.com';
$user->save();

これを実行すると、ユーザーデータが更新され、historiesupdatedの新しいデータが追加されました。

次に、このユーザーを削除します。

$user = \App\User::find(1);
$user->delete();

これを実行するとユーザーテーブルは空になり、さらにhistoriesdeletedのデータが追加されました。

では最後に、削除したデータをhistoriesテーブルID:3(つまり削除直前)に戻してみましょう。

$history = \App\History::find(3);
$history->revert();

実行後はこうなりました。

はい❗削除したデータが元も戻り、さらに元に戻したことがわかるrevertedのデータがhistoriesに追加されています。

成功です😊✨

ダウンロードする

今回実際に開発したソースコード一式を以下からダウンロードすることができます。

【Laravel】DB操作のrevert(時を戻そう)機能をつくる

※ただし、usersテーブルなどはご自身で作成する必要があります。

おわりに

今回はとてもシンプルな構成で実装をしてみましたが、1点だけ気になるのが、「データ量の多さ」です。

つまり、たくさんのデータを何度も書き換える必要があるシステムではデータ量が多くなってしまう可能性があるので、そういった場合は重複する部分を省略できるように「何のどこを変更したか」というような省エネ設計にする必要があると思います。

ただ、そうなると複雑になってしまうので今後時間&アイデアが思い浮かんだら挑戦してみます👍(Dropboxとかgitとかはそんな感じなのかな??)

ともあれ、ぜひ皆さんも試してみてくださいね。

ではでは〜!


「チー牛ってそんな意味なの❗❓😂」

今回の技術をつかった開発のご依頼、お待ちしております😊✨ お問い合わせ また、個人レッスンや、わかりにくい部分がありましたらからお気軽にご連絡ください。 どうぞよろしくお願いいたします!
このエントリーをはてなブックマークに追加       follow us in feedly