LaravelでPostgreSQLの条件付きユニーク制約を作る

## 概要
ユニーク制約を付けたいが、論理削除のため、普通のやり方では、制約が作成できない。
そういったときに、下記のような条件付きユニーク制約を設定したい。

```sql: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」フォルダを作って、そこに独自定義のクラスを作成する。

```php: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」を作成する。

```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で使ってみる

```php: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');
    }
}
```

以上

## 参考サイト
- [Extend Blueprint class?](https://stackoverflow.com/questions/22444685/extend-blueprint-class)
- [How to set TimeStamp with Timezone in Laravel 5 Migrate](https://stackoverflow.com/questions/29557419/how-to-set-timestamp-with-timezone-in-laravel-5-migrate/31467329#31467329)
- [Laravel5 マイグレーションファイルで素のSQLクエリを実行する](http://shibukixxx.hateblo.jp/entry/2015/10/09/105329)