放浪軍師のアプリ開発局

VTuberみたいなアプリケーション夏狂乱など、自由気ままにアプリを開発したりしています。他にもいろいろやってます。尚、このブログはわからないところを頑張って解決するブログであるため、正しい保証がありません。ご注意ください。

Laravel でバックエンド 【Migration 基礎・小ネタ 編】

放浪軍師です。しばらくは Laravel の小技紹介いくよー!今回はデータベースのテーブルを作成したりできる Migration のお話。

Migration

laravel では Migration と呼ばれるデータベースへのテーブルおよびカラムの作成や編集などを行う機能があります。 バージョン管理のような仕組みが備わっているのが特徴的で、未実行のデータベース操作のみを起動したり、巻き戻したりといった事が簡単にできます。

readouble.com

環境

基本

php artisan make:migration コマンドで migration 用のファイルを作成できます。例えば、php artisan make:migration create_companies_table コマンドを実行すると、database/migrations2023_11_08_160815_create_companies_table.php のような、日付付きのファイルが作成されます。中身はこんな感じ。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('companies', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('companies');
    }
};

up() は php artisan migrate を実行した際に動作するメソッドで、down() は php artisan migrate:rollback を実行した際に動作するメソッドです。早速 php artisan migrate を実行してマイグレーションをしてみます。すると、以下のように表示されるかと思います。

INFO  Running migrations.  
2023_11_08_160815_create_companies_table .............. 22ms DONE

データベースを確認すると、companies テーブルができあがっていて、カラムは id, created_at, update_at ができているのが確認できます。これは up() メソッドが動作した結果で、内訳は

<?php
public function up(): void
{
    Schema::create('companies', function (Blueprint $table) { // companies テーブル作成
        $table->id(); // id カラム作成
        $table->timestamps(); // created_at カラムと updated_at カラム作成
    });
}

という感じになります。また、この状態で php artisan migrate:rollback を実行してみます。すると、以下のように表示されます。

INFO  Rolling back migrations.
2023_11_08_160815_create_companies_table .............. 22ms DONE

データベースを確認すると、companies テーブルが削除されているのが確認できます。これは down() メソッドが動作した結果で、内訳は

<?php
public function down(): void
{
    Schema::dropIfExists('companies'); // companies テーブル削除
}

となります。要は up() でデータベースに与えたい影響を記述し、down() にそれを戻す記述をすれば良いという事だけ覚えておけば良いです。

小技

ここからは小技紹介です。どれも活躍の場があると思いますので、記憶の片隅にでも置いておくと助かる場面があるかもしれません。

複合ユニーク名長すぎ問題の回避

複数のカラムを対象としたユニーク制約は以下のように書きます。

<?php
public function up(): void
{
    Schema::create('hogehoge_fugafugas', function (Blueprint $table) {
        $table->id();
        $table->string('hogehoge_hogehoge_hogehoge');
        $table->string('fugafuga_fugafuga_fugafuga');
        $table->timestamps();
        $table->unique(['hogehoge_hogehoge_hogehoge', 'fugafuga_fugafuga_fugafuga']); //複合ユニークキー
    });
}

ただ、これを実行すると以下のようなエラーが発生してしまいます。

SQLSTATE[42000]: Syntax error or access violation: 1059 Identifier name 'hogehoge_fugafugas_hogehoge_hogehoge_hogehoge_fugafuga_fugafuga_fugafuga_unique' is too long

複合ユニークを張る場合、制約名は自動で テーブル名_カラム名1_カラム名2_unique のように付きますが、それが長すぎると言う警告で、カラム名が長すぎたりすると発生します。解決するには、

<?php
public function up(): void
{
    Schema::create('hogehoge_fugafugas', function (Blueprint $table) {
        $table->id();
        $table->string('hogehoge_hogehoge_hogehoge');
        $table->string('fugafuga_fugafuga_fugafuga');
        $table->timestamps();
        $table->unique(
            ['hogehoge_hogehoge_hogehoge', 'fugafuga_fugafuga_fugafuga'], //複合ユニークキー
            'hogehoge_fugafugas_unique' //第二引数で制約名を指定
        );
    });
}

このように unique() の第二引数で短めの制約名を付ければ良いです。

複合ユニークと論理削除の両立

テーブルに論理削除を指定する場合、以下のように書きます。

<?php
public function up(): void
{
    Schema::create('users', function (Blueprint $table) {
        $table->id();
        $table->foreignId('company_id')->constrained();
        $table->string('name');
        $table->timestamps();
        $table->softDeletes(); //論理削除を指定。deleted_atカラムが追加される。
    });
}

そして、Model に use SoftDeletes; を追加すると、Model を経由した削除(User::find(1)->delete()など)した際には物理削除せずに deleted_at カラムに日付が入る形で論理削除扱いとなります。この状態であれば、Model を経由する呼出(User::find(1)など)ではヒットしなくなります。

ただ、この論理削除と前述した複合ユニークを組み合わせると問題が発生します。

<?php
public function up(): void
{
    Schema::create('users', function (Blueprint $table) {
        $table->id();
        $table->foreignId('company_id')->constrained();
        $table->string('name');
        $table->timestamps();
        $table->softDeletes(); //論理削除を指定。deleted_atカラムが追加される。
        $table->unique(['company_id', 'name']); //複合ユニーク
    });
}

この状態で論理削除した後、また同じ複合ユニーク条件を満たすようなデータを作成しようとした場合にエラーになります。具体的に言えば、会社を辞めたので社員を削除した後、その社員が復帰したのでまた登録しようとした場合などですね。複合ユニークは論理削除を考慮してくれないわけです。解決するには、

<?php
public function up(): void
{
    Schema::create('users', function (Blueprint $table) {
        $table->id();
        $table->foreignId('company_id')->constrained();
        $table->string('name');
        $table->timestamps();
        $table->softDeletes(); //論理削除を指定。deleted_atカラムが追加される。
        $table->boolean('not_deleted')
            ->nullable()
            ->virtualAs('case when deleted_at is null then 1 else null end'); // deleted_at が null なら 1 そうでなければ null が自動的に入るカラムを作成
        $table->unique(['company_id', 'name', 'not_deleted']); //複合ユニークキーに not_deleted を含める
    });
}

このように、特殊な not_deleted カラムを作成して、それを複合ユニークキーに含めます。これはいずれかの複合ユニークキーに null が指定されている場合は複合ユニークから除外される事を使ったテクニックで、論理削除を行うと not_daleted カラムに null が入ることにより複合ユニークの条件から除外され、再度同じ条件を満たすデータが入れられるようになると言うものです。なお、virtualAs() は実体のないバーチャルカラムを作成していますが、環境によっては使えないのでその場合は storedAs() にして物理カラムを作成するようにしてください。

外部キー制約一時解除

外部キーを既存のテーブルに追加する場合は、以下のように書きます。

<?php
public function up(): void
{
    Schema::table('users', function (Blueprint $table) {
        $table->foreignId('company_id')->constrained(); //外部キー追加
    });
}

ただ、既にデータが入っている場合、以下のようなエラーが出ます。

SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (`mySql`.`#sql-1_1302`, CONSTRAINT `users_company_id_foreign` FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`))

外部キー制約にひっかかったというエラーです。外部キーを追加するということは、その対象テーブルに存在するキー(今回なら companies テーブルの既存 id)を挿入しなければならないが、新規にカラムを追加すると初期値の 0 になり、id = 0 のレコードが対象のテーブルに存在しないのでエラーになるというロジックです。解決するには、

<?php
public function up(): void
{
    DB::statement('SET FOREIGN_KEY_CHECKS=0;'); //外部キー制約解除
    Schema::table('users', function (Blueprint $table) {
        $table->foreignId('company_id')->constrained(); //外部キー追加
    });
    DB::statement('SET FOREIGN_KEY_CHECKS=1;'); //外部キー制約設定
}

このように追加時に外部キー制約を一時的に解除しておけば、追加したカラムに 0 が入ってマイグレーションが完了します。ただ、データ的には破綻した状態ですので、マイグレーション後には正しい外部キーを seeder で挿入してやるというのを忘れないようにしてください。

マイグレーションでレコードを挿入してはいけない

マイグレーションでマスターテーブルの初期レコードを挿入するという旨の記事をそこそこ見かけます。例えば以下のような感じです。

<?php
public function up(): void
{
    Schema::create('states', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->timestamps();
    });

    // マスターデータの挿入
    State::create(['id' => 1, 'name' => '開始前']); 
    State::create(['id' => 2, 'name' => '処理中']);
    State::create(['id' => 3, 'name' => '処理後']);
}

ただ、これは絶対にやめた方がいいです。何故ならマスターデータの変更や追加に対応できないからです。毎回マイグレーションファイルを作りますか?とっても不効率です。また、データの移行とかもやめた方がいいです。バージョン管理も担うマイグレーションファイルは、マイグレーション実行後は編集してはいけない性質を持っているので、そこで使用している Model クラスを消す事ができなくなってしまいます。マイグレーションファイルはデータベース操作のみに使用して、レコードの挿入は Seeder に任せましょう。マスターデータであれば、DatabaseSeeder から該当 Seeder を呼び出せるようにおけば、php artisan migrate --seed コマンドでマイグレーション後にマスターデータの挿入まで一度に実行できます。

テーブル名は複数形スネークケースにしよう

例えば会社名であれば、companies 。会社名詳細なら company_details のように、テーブル名は複数形スネークケースに固定しましょう。これは後に作成する Eloquent モデルでテーブル名を指定しない場合は複数形スネークケースを自動で使用する為です。どうせならコードは少ない方が良いですし、混乱の元ですからね。よほど理由がない限りは守った方が良いと思います。

データが入った状態でのマイグレーションロールバックを必ず試しておく

マイグレーションは、新規で実行するとうまくいくが、データが存在すると失敗するといった事がビックリするぐらい良くあります。そのため、マイグレーションファイルを作成した後やレビュー担当時には修正前のブランチでいくつかレコードが入っている状態を準備した後、マイグレーションロールバックを数回繰り返して、問題が発生しないかを必ず確認しましょう。これを怠ると、本番環境でロールバックした際にエラーが発生して、ただでさえロールバックするような問題が発生している状態で追加ダメージを受けることになり、大いに肝を冷やします。必ずやりましょう。

本番環境でマイグレーションする前にはバックアップを必ず取る

本番環境でマイグレーションを実行する場合、たとえ単純なマイグレーションだったとしてもバックアップを取りましょう。もし万が一マイグレーションの実行を含むリリースでなにか問題が発生して、ロールバックを実行する事にしたとします。もし、そのマイグレーション失敗していた場合は、今回ではなく、その前に実行したマイグレーションが戻ります。つまり既にデータが入って運用に乗っているであろうテーブルやカラムが飛びます。マジヤバです。顔面蒼白しますマイグレーションを実行するということは、ロールバックを実行する可能性があると言う事。かならずバックアップを取りましょう。取り返しがつかなくなる恐れがあります。

まとめ

以上がマイグレーションの基本および小ネタになります。最後にゾッとする小ネタ挟んでごめんね!でも大事だからね!ではでは楽しい Laravel ライフを!