
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、これは私に限らないかもしれませんが、何気なく過ごしていると「そういえばアレ面白かったな〜」とクスっとしてしまったりします。
そして先日はその影響で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
とかはそんな感じなのかな??)
ともあれ、ぜひ皆さんも試してみてくださいね。
ではでは〜!
「チー牛ってそんな意味なの」