December 11, 2021 • ☕️ 4 min read
【 環境 】
Laravel のバージョン: 8.16.1
PHP のバージョン: 7.4.7
MySQL のバージョン: 5.7
テーブルにユニークキーを設定する場合、null を許可したカラムを含めると、意図しない重複レコードが作成されてしまう事があります。
主に、削除処理を論理削除に設定して、deleted_at といったカラムを設定している時に発生するケースがあるかと思われます。
以下、テーブルとデータの例。
public function up()
{
Schema::create('items', function (Blueprint $table) {
$table->id();
$table->bigInteger('owner_id')->default(0);
$table->string('code', 80);
$table->string('name', 100);
$table->string('sub_name', 50)->nullable();
$table->integer('price')->default(0);
$table->integer('category_id')->nullable()->unsigned();
$table->timestamps();
$table->softDeletes();
$table->unique(['owner_id', 'code', 'deleted_at'], 'items_unique_key');
});
}
CREATE TABLE `items` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`owner_id` bigint(20) NOT NULL DEFAULT '0',
`code` varchar(80) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`sub_name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`price` int(11) NOT NULL DEFAULT '0',
`category_id` int(10) unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `items_unique_key` (`owner_id`,`code`,`deleted_at`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
ユニークキー項目:owner_id, code, deleted_at
id | owner_id | code | name | deleted_at |
---|---|---|---|---|
1 | 1 | PRD-001 | product-001 | « NULL » |
2 | 1 | PRD-001 | product-001 | « NULL » |
こんな感じで、キーが重複したレコードが存在できてしまいます。
null が入っている場合、ユニーク制約は効かないようです。
上記の場合、deleted_at をユニークキー項目に含めていますが、この値が null となっており、ユニークキーチェックの制御から外れてしまい、重複レコードが存在できるようになってしまいます。
ちなみに、deleted_at に何かしらの値が入った場合は、ユニークキー制御が働き、重複レコードが存在しない状態が担保されます。
こちらを参考にさせて頂きました。
MySQLでLaravel標準のSoftDeletesを使った論理削除とユニーク制約を両立させる方法
「論理削除されていれば NULL, されていなければ 1 になる生成カラムを定義」という方法です。
これにより、「ユニークキー制御となるカラムには、必ず何かしらの値が入るようにする」という状態を保持できます。
Schema::create('items', function (Blueprint $table) {
$table->id();
$table->bigInteger('owner_id')->default(0);
$table->string('code', 80);
$table->string('name', 100);
$table->string('sub_name', 50)->nullable();
$table->integer('price')->default(0);
$table->integer('category_id')->nullable()->unsigned();
$table->timestamps();
$table->softDeletes();
$table->boolean('existence')->nullable()->storedAs('CASE WHEN deleted_at IS NULL THEN 1 ELSE NULL END');
$table->unique(['owner_id', 'code', 'existence'], 'items_unique_key');
});
CREATE TABLE `items` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`owner_id` bigint(20) NOT NULL DEFAULT '0',
`code` varchar(80) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`sub_name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`price` int(11) NOT NULL DEFAULT '0',
`category_id` int(10) unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
`existence` tinyint(1) GENERATED ALWAYS AS ((case when isnull(`deleted_at`) then 1 else NULL end)) STORED,
PRIMARY KEY (`id`),
UNIQUE KEY `items_unique_key` (`owner_id`,`code`,`existence`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
「existence」のカラムはユーザが自由に編集する事ができず、deleted_at の値に依存します。
こうする事で、null を取り得るカラムには強制的に何かしらの値が入るようにし、ユニークキー制御が必ず働くように制御しています。