九保すこひ@フリーランスエンジニア|累計300万PVのブログ運営中
さてさて、これは私に限らないかもしれませんが、何気なく過ごしていると「そういえばアレ面白かったな〜」とクスっとしてしまったりします。
そして先日はその影響で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);
となります。
さぁ、これで準備は完了しました❗
テストしてみる
ではusers
とhistories
テーブルを空にしてテストしてみます。
まずは、ユーザー追加です。
$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();
これを実行すると、ユーザーデータが更新され、histories
にupdated
の新しいデータが追加されました。
次に、このユーザーを削除します。
$user = \App\User::find(1); $user->delete();
これを実行するとユーザーテーブルは空になり、さらにhistories
にdeleted
のデータが追加されました。
では最後に、削除したデータをhistoriesテーブル
のID:3
(つまり削除直前)に戻してみましょう。
$history = \App\History::find(3); $history->revert();
実行後はこうなりました。
はい❗削除したデータが元も戻り、さらに元に戻したことがわかるreverted
のデータがhistories
に追加されています。
成功です😊✨
ダウンロードする
今回実際に開発したソースコード一式を以下からダウンロードすることができます。
【Laravel】DB操作のrevert(時を戻そう)機能をつくる※ただし、usersテーブルなどはご自身で作成する必要があります。
おわりに
今回はとてもシンプルな構成で実装をしてみましたが、1点だけ気になるのが、「データ量の多さ」です。
つまり、たくさんのデータを何度も書き換える必要があるシステムではデータ量が多くなってしまう可能性があるので、そういった場合は重複する部分を省略できるように「何のどこを変更したか」というような省エネ設計にする必要があると思います。
ただ、そうなると複雑になってしまうので今後時間&アイデアが思い浮かんだら挑戦してみます👍(Dropbox
とかgit
とかはそんな感じなのかな??)
ともあれ、ぜひ皆さんも試してみてくださいね。
ではでは〜!
「チー牛ってそんな意味なの❗❓😂」