概要
ユニーク制約を付けたいが、論理削除のため、普通のやり方では、制約が作成できない。
そういったときに、下記のような条件付きユニーク制約を設定したい。
PostgreSQL
CREATE UNIQUE INDEX users_unique_email
ON users
USING btree (email COLLATE pg_catalog."default")
WHERE deleted_at;
ただし、Laravelのmigrateの通常の機能では対応できないようなので拡張を検討したメモを残します。
SQLを直接実行を調査
SQLを実行できれば、対応可能だな〜と思い調べてみたところ、下記の記事を見つける。
Laravel5 マイグレーションファイルで素のSQLクエリを実行する
http://shibukixxx.hateblo.jp/entry/2015/10/09/105329
しかし、Laravelのmigrateは、実行するSQLを変数にプールしておき、
まとめて実行するようなので、この直実行は、タイミングが合わず、
テーブルが存在しないとエラーになる、残念...。
ちゃんと調べてみることにする。
現状調査
調べてみた結果、PostgreSQLのmigrateには下記のファイルが関係しているようです。
- Illuminate\Database\Schema\Blueprint;
- Illuminate\Database\Schema\Grammars\PostgresGrammar;
それぞれを継承して拡張してみる。
CustomBlueprint作成
「app/Migrates」フォルダを作って、そこに独自定義のクラスを作成する。
app/Migrates/CustomBlueprint.php
<?php
namespace App\Migrates;
use Illuminate\Database\Schema\Blueprint;
/**
* Blueprintクラスの拡張
*
* @package App\Migrate
*/
class CustomBlueprint extends Blueprint
{
/**
* 部分インデックスを利用したユニーク制約
*
* ※ 主に論理削除などに利用
*
* PostgreSQL Document
* https://www.postgresql.jp/document/9.1/html/indexes-partial.html
*
* @param string $column
* @param string $index
* @param string $where
* @return \Illuminate\Support\Fluent
*/
public function uniquePartial($column, $index, $where = null)
{
$index = $index ? : $this->createIndexName('unique', (array) $column);
return $this->addCommand(
'uniquePartial', compact('index', 'column', 'where')
);
}
}
CustomPgsqlGrammar作成
次に「CustomPgsqlGrammar.php」を作成する。
app/Migrates/CustomPgsqlGrammar.php
<?php
namespace App\Migrates;
use Illuminate\Support\Fluent;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Grammars\PostgresGrammar;
/**
* PostgresGrammarクラスを拡張
*
* @package App\Migrate
*/
class CustomPgsqlGrammar extends PostgresGrammar
{
/**
* 条件付きユニーク制約を作成する
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileUniquePartial(Blueprint $blueprint, Fluent $command)
{
return sprintf('create unique index %s on %s using btree (%s collate pg_catalog."default") where %s',
$this->wrap($command->index),
$this->wrapTable($blueprint),
$this->wrap($command->column),
$command->where
);
}
}
migrateで使ってみる
database/migrations/****_**_**_******_create_users_table.php
<?php
use Illuminate\Support\Facades\Schema,
Illuminate\Database\Schema\Blueprint,
Illuminate\Database\Migrations\Migration,
App\Migrates\CustomBlueprint,
App\Migrates\CustomPgsqlGrammar;
/**
* 会員テーブルの作成
*/
class CreateUsersTable extends Migration
{
/**
* マイグレーション実行
*
* @access public
* @return void
*/
public function up()
{
DB::connection()->setSchemaGrammar(new CustomPgsqlGrammar());
$schema = DB::connection()->getSchemaBuilder();
$schema->blueprintResolver(function($table, $callback) {
return new CustomBlueprint($table, $callback);
});
$schema->create('users', function (CustomBlueprint $table) {
$table->bigincrements('id');
$table->string('email');
$table->password('password');
$table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP'));
$table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP'));
// 削除日時 (論理削除フラグ)
$table->softDeletes();
// ユニーク制約 (論理削除対応)
$table->uniquePartial('email', null, 'deleted_at is null');
});
}
/**
* 元に戻す
*
* @access public
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
}
以上