DB保存時に共通列を自動更新する方法 [PHP Laravel 開発効率化]

 PHP Laravelを用いた開発時の事ですが、全テーブルが保持する共通列の更新を、自動で行ってほしい場面がありました。

 今回は、Eloquentで共通列を簡単に更新する方法がありましたので、その時の実装方法を紹介したいと思います。

環境

・Laravel 9
・PHP 8.0
・MySQL 5.7
 

Eloquentとは

 そもそもLaravelのEloquentとは何でしょうか。

 LaravelのEloquentとは、DBのテーブルに対応するモデルを定義することで、DBの操作を行いやすくするORM(Object-Relational Mapping)の事です。

 Eloquentを使用することで、SQLを直接書くことなく、モデルに対して操作を行えるようになります。

 EloquentにはDBにアクセスするための便利なメソッドや機能が多数用意されています。

 Eloquentを使用することで、複雑なクエリを簡単に組み立てることができたり、関連するテーブル間で自動的にデータを取得することができたりするなど、効率的かつ柔軟なDB操作が可能になります。

 簡単な例として、素のPHPのみ、Laravelのクエリビルダ、LaravelのEloquentの3つの方法でデータを取得する方法を挙げます。

1. 素のPHPの場合

$dsn = 'mysql:dbname=database;host=localhost';
$user = 'user';
$password = 'password';
$dbh = new PDO($dsn, $user, $pass);

// データを取得
$sql = 'SELECT * FROM users WHERE user_id=?';
$stmt = $dbh->prepare($sql);
$data[] = $id;
$stmt->execute($data);
$rec = $stmt->fetch(PDO::FETCH_ASSOC);
if($rec!=false)
{
    // ユーザ名を取得
    $user_name = $rec['name'];
}

// DBから切断
$dbh = null;

2. クエリビルダを使用する場合

// DBからデータを取得
$user = DB::table('users')->where('id', $id)->select('name')->first();

// ユーザ名を取得
$user_name = $user->name;

3. Eloquentを使用する場合

// DBからデータを取得
$user = User::find($id);

// ユーザ名を取得
$user_name = $user->name;

 上記の例では、`users`テーブルから`id`でデータを取得後、`name`の列を取得しています。

 それぞれの方法で同じ結果が得られますが、Elequentを使用する場合、Modelクラスを利用するため、よりオブジェクト指向的なコードを書くことができます。

 

経緯

 今回使用するDBのテーブルには共通列として作成日時、作成者ID、更新日時、更新者IDが定義されていました。

 以下に例としてユーザマスタテーブル(users)の構成を挙げます。

論理名 物理名
ユーザID id
ユーザ名 name
メールアドレス email
パスワード password
作成日時 created_at
作成者ID created_user_id
更新日時 updated_at
更新者ID updated_user_id

 作成日時、更新日時はEloquentのモデルにデフォルトで自動管理する機能があるのでそれを利用します。

 問題は作成者IDと更新者IDです。

 それぞれの列にはその時の操作をしたユーザのIDを入れる仕様になっています。

 作成、更新時に毎回更新処理を記述すれば済む話ではあるのですが、それだとどこかで漏れが発生する可能性がありますし、何より面倒くさいです。

 ということで、それらを記載せずとも保存時に自動で入力してくれる機能はないかと思い立ったのが今回の経緯になります。

$user = new User();
$user->name = $user_name;
$user->email = $email;
$user->password = Hash::make($password);
$user->created_user_id = Auth::user()->id; // ←これを毎回書きたくない!!
$user->updated_user_id = Auth::user()->id; // ←これを毎回書きたくない!!
$user->save();

 

詳細

 今回実際に自動更新機能を実装した手順になります。

①オブザーバーを作成

 作成時、更新時の処理を入れたオブザーバーを実装します。

[App\Observers]直下にオブザーバークラスを作成します。

 オブザーバーは手動で作成してもよいですがartisan作成コマンドが用意されているためそちらを使うのが無難でしょう。

 今回はModelObserverというクラス名で作成しました。

コマンド:

php artisan make:observer ModelObserver

 オブザーバークラスを作成したら、次に内部の処理を記述していきます。

 今回は作成時に作成者ID、更新時に変更内容があれば更新者IDを更新するようにします。

実際のコード:App\Observers\ModelObserver.php

<?php

namespace App\Observers;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;

class ModelObserver
{
    public function creating(Model $model)
    {
        // 作成者IDを更新
        $model->created_user_id = Auth::user()->id;
    }

    public function updating(Model $model)
    {
        // 変更があれば
        if($model->isDirty())
        {
            // 更新者IDを更新
            $model->update_user_id = Auth::user()->id;
        }
    }
}

②トレイトを作成

 次にオブザーバーを呼び出すトレイトを作成します。

 トレイト用のartisan作成コマンドは用意されていないため手動で作成しました。

 [App\Traits]フォルダを作成し、その直下に[ModelObservable.php]ファイルを作成します。

 ファイルを作成したら、そこにオブザーバーを呼び出すトレイトを定義します。

実際のコード:App\Traits\ModelObservable.php

<?php
namespace App\Traits;

use App\Observers\ModelObserver;

trait ModelObservable
{
    public static function bootModelObservable()
    {
        // オブザーバーを呼び出す
        self::observe(ModelObserver::class);
    }
}

③各モデルにトレイトを呼び出す処理を追記

 最後に、それぞれのモデルで作成したトレイトを呼び出したら完了です。

実際のコード例:App\Models\User.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Traits\ModelObservable;         // これを追記

class User extends Model
{
    use HasFactory, ModelObservable;    // これを追記
}

 

まとめ

 作成者ID、更新者IDのように常に一定の値が入る列を自動で更新したいときは、オブザーバー、トレイトを定義することで、毎回更新処理を記述する必要なく共通列が自動で更新される機能を実装することができました。

 

補足

 後日改めて調べたところ、オブザーバーを作成してそれをプロバイダーに登録するという実装方法もあるそうです。(むしろこちらが一般的っぽい?)

 補足までにそちらの方法も紹介します。

 ①までは先述と同じです。

 オブザーバーの作成後、[App\Providers\EventServiceProvider.php]のboot()メソッドに追記してプロバイダーに作成したオブザーバーを登録します。

実際のコード:App\Providers\EventServiceProvider.php

<?php

namespace App\Providers;

use App\Models\User;                // これを追記
use App\Observers\ModelObserver;    // これを追記

class EventServiceProvider extends ServiceProvider
{
    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        User::observe(ModelObserver::class);    // これを追記
    }
}

 オブザーバーを使いたいモデルごとに追記する必要はありそうですが、先に挙げた方法と異なりトレイトを作成する必要がなく、各モデルに追記する必要もないためこちらの方がよいかもしれません。

 また、EventServiceProvider$observersプロパティへオブザーバーをリストすることでも登録が可能だそうです。

プロパティに追加する場合:App\Providers\EventServiceProvider.php

<?php

namespace App\Providers;

use App\Models\User;                // これを追記
use App\Observers\ModelObserver;    // これを追記

class EventServiceProvider extends ServiceProvider
{
    /**
     * アプリケーションのモデルオブザーバ
     *
     * @var array
     */
    protected $observers = [
        User::class => [ModelObserver::class],  // これを追記
    ];
}

 

蛇足

 LaravelのEloquentにはタイムスタンプを自動更新する機能がデフォルトで搭載されています。

「デフォルトでEloquentは、モデルと対応するDBテーブルに、created_atカラムとupdated_atカラムが存在していると想定します。Eloquentはモデルが作成または更新されるときに、これらの列の値を自動的にセットします。」

引用:主キータイムスタンプ | Laravel 9.x Eloquentの準備

 上記の通り、デフォルトでcreated_atとupdated_atは自動更新されるため、今回のオブザーバーにこれらの更新処理は含めていません。

 また、逆にタイムスタンプはテーブル定義に含まれないから自動更新機能をOFFにしたいという場合はモデルの$timestampsプロパティをfalseにすることで実現できます。

 タイムスタンプの列はあるが列名が異なる場合は、それぞれCREATED_AT及びUPDATED_AT定数を定義することで自動更新される列を指定できます。

<?php

class User extends Model
{
    /**
     * モデルにタイムスタンプをつけるか
     *
     * @var boolean
     */
    public $timestamps = false;

    // タイムスタンプのカラム名
    const CREATED_AT = 'registered_at';
    const UPDATED_AT = 'updated_at';
}

 お役に立てましたでしょうか? もし少しでも役に立ったのでしたら、いいね! もしくは、SNSなどで共有してくださると嬉しいです。

 

 最後まで読んで下さりありがとうございました。

 

お問い合わせはこちら