【Laravel】論理削除とユニーク制約を両立させる【MySQLとMariaDB】

技術メモ

論理削除は便利ですが、DBのユニーク制約と両立させるには一工夫必要です。

今回はLaravelで論理削除とユニーク制約を両立させる方法について解説します。

 

論理削除してもDBのユニーク制約が機能してしまう!

Laravelで論理削除を実装する際は、deleted_atカラムを利用します。

deleted_atカラムに日付が入っていればそのレコードは削除されたとみなし、NULLの場合は存在しているとみなします。

 

しかし、論理削除をしても実際にはレコードがテーブルに存在しているため、同一の値を設定するとユニーク制約に引っかかります。

 

例を示します。

emailカラムがユニークであるusersテーブルが存在するとします。

 

2014_10_12_000000_create_users_table.php

<?php 

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

class CreateUsersTable extends Migration 
{
    public function up() 
    { 
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();

            // 論理削除
            $table->softDeletes();
        });
    }

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

 

User.phpにuse SoftDeletes;を記載して、論理削除機能を有効にします。

 

User.php

<?php

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;
    use SoftDeletes;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

 

これをマイグレーションして、usersテーブルを確認してみます。

usersテーブルのカラム設定

 

usersテーブルにはレコードが一件存在します。

 

このレコードを論理削除して再度同じメールアドレスでユーザーを作成してみます。

論理削除する場合は、deleted_atカラムに日付を入力します。

 

UPDATE users SET deleted_at=NOW() WHERE id=1;

 

 

 

論理削除した状態でユーザーを作成すると、ユニーク制約に引っかかりエラーが発生します。

INSERT INTO users VALUES(2,'tanake','tanaka@gmail.com',NULL,SHA2('password',256),NULL,NOW(),NOW(),NULL);

 

 

ERROR 1062 (23000): Duplicate entry 'tanaka@gmail.com' for key 'users.users_email_unique'

 

論理削除とユニーク制約を共存させる方法

論理削除とユニーク制約を共存させるには、複合ユニーク制約を利用します。

複合ユニーク制約とは、複数のカラムの組み合わせに対してユニーク制約を設定することです。

 

複合ユニーク制約の「NULLの場合ユニーク制約が無効になる」という特徴を利用して論理削除とユニーク制約を両立させることが可能です。

 

 

論理削除とユニーク制約を両立させる流れを説明します。

 

1. 「exist」というカラムを作成

「exist」というカラムを新たに用意して、生成列とします。

生成列(Generated Columns)とは、他のカラムの値を利用して自動で計算して値を表示してくれる列のことです。
詳しい説明はMySQL公式を確認してください。

 

生成列の定義を以下のようにします。

deleted_atカラムがNULLのとき(レコードが存在している場合)は、existカラムを1にする。

deleted_atカラムがNULLではないとき(レコードが論理削除されている場合)は、existカラムをNULLにする。

 

2. existカラムを含む複合ユニーク制約を設定する

今回はemailにユニーク制約を設定していたため、emailとexistの複合ユニーク制約とします。

 

これでdeleted_atがNULLのときはexistが1になるのでemailのユニーク制約が機能します。

レコードが論理削除された場合はdeleted_atに日付がはいるため、existにNULLが設定されユニーク制約が無効になります。

 

ユニーク制約が無効になるため、再度同じメールアドレスを登録することが可能です。

 

MySQLの場合はStoreAsを利用

MySQLの場合は、マイグレーションファイルでStoreAsを利用することで生成列(Generated Columns)を定義できます。

 

2014_10_12_000000_create_users_table.php

<?php 

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

class CreateUsersTable extends Migration 
{
    public function up() 
    { 
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('email');
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();

            // 論理削除
            $table->softDeletes();

            // 論理削除されていれば NULL, されていなければ 1 になる生成列を定義
            $table->boolean('exist')->nullable()->storedAs('case when deleted_at is null then 1 else null end');

            // 複合ユニーク制約
            $table->unique(['email', 'exist']);
        });
    }

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

 

マイグレーションして、テーブルの状態を確認します。

 

 

それでは論理削除とユニーク制約が両立しているか確認してみます。

すでに存在するメールアドレスを再度インサートしてみると、ユニーク制約によりエラーになります。

existカラムの値が1になっていることに注目してくだい。

 

 

論理削除後、同一のメールアドレスをインサートしてみます。

 

無事同一のメールアドレス「tanaka@gmail.com」をインサートすることができました。

id=1のレコードは論理削除されているため、existカラムが自動的にNULLになります。

そのため複合ユニーク制約が無効となり、emailカラムに同一の値を設定することが可能になっています。

 

MariaDBの場合は素のSQLを書く

MariaDBはStoreAsを利用することができないので、素のSQLを書く必要があります。

 

2014_10_12_000000_create_users_table.php

<?php 

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

class CreateUsersTable extends Migration 
{
    public function up() 
    { 
        DB::statement('
                create table users (
                    id bigint unsigned not null auto_increment primary key,
                    name varchar(255) not null,
                    email varchar(255) not null,
                    email_verified_at timestamp null,
                    password varchar(255) not null,
                    remember_token varchar(100) null,
                    created_at timestamp null,
                    updated_at timestamp null,
                    deleted_at timestamp null,
                    exist tinyint(1) as (case when deleted_at is null then 1 else null end) stored,
                    unique key (email, exist)
                ) default character set utf8 collate "utf8_general_ci";
            ');
    }

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

 

やっていることはMySQLと同じです。

これでMySQLと同様に論理削除とユニーク制約を両立させることができます。

 

今回の記事で分かりにくいところや、他にも解説して欲しい内容があれば、是非ともコメント欄やお問い合わせからメッセージをください!

 

これから「Laravelを勉強したい!」と考えている人は一冊書籍を持っておくと体系的に勉強できます。

Laravelを使用したアプリ開発を学ぶのであれば「青本」がおすすめです。

私もこの書籍から入りました!

 

 

「Webサービスを作りたい!」という方に向けて記事を書きました。

 

業務未経験からポートフォリオなしでWebエンジニアに転職した際の経験談を語りました。

技術メモ
スポンサーリンク
スポンサーリンク
のすけをフォローする
やばブロ!

コメント