【Laravel】 MySQL の生成カラムに NOT NULL 制約を付与する方法

# 情報更新

Laravel 6.x で以下のプルリクが採用され，フレームワーク標準で対応できるようになりました！

- [[Proposal] Add schema method `storedAsNotNull` and `virtualAsNotNull` · Issue #1211 · laravel/ideas](https://github.com/laravel/ideas/issues/1211)
- [[6.x] Change MySql nullable modifier to allow generated columns to be not null by DrCake · Pull Request #31452 · laravel/framework](https://github.com/laravel/framework/pull/31452)

`->storedAsNotNull()` の代わりに **`->storedAs()->nullable(false)`** を使用してください。

-----


# 動機

[MySQL :: MySQL 5.7 Reference Manual :: 13.1.18.8 CREATE TABLE and Generated Columns](https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html)

```sql
col_name data_type [GENERATED ALWAYS] AS (expression)
  [VIRTUAL | STORED] [NOT NULL | NULL]
  [UNIQUE [KEY]] [[PRIMARY] KEY]
  [COMMENT 'string']
```

見ての通り，MySQLの生成カラムには`NOT NULL`制約を付与することができる。これにより，誤った値の組み合わせが別のカラムに入っているときに`NULL`になるように生成カラムの式を書くと，柔軟な条件で不整合を防ぐことができる。

[database/MySqlGrammar.php at 16980ffecb15e47d0a1942a64d3cc548e22f709c · illuminate/database](https://github.com/illuminate/database/blob/16980ffecb15e47d0a1942a64d3cc548e22f709c/Schema/Grammars/MySqlGrammar.php#L869-L881)

```php
    /**
     * Get the SQL for a nullable column modifier.
     *
     * @param  \Illuminate\Database\Schema\Blueprint  $blueprint
     * @param  \Illuminate\Support\Fluent  $column
     * @return string|null
     */
    protected function modifyNullable(Blueprint $blueprint, Fluent $column)
    {
        if (is_null($column->virtualAs) && is_null($column->storedAs)) {
            return $column->nullable ? ' null' : ' not null';
        }
    }
```

しかしお節介なことに，Laravelでは`->virtualAs()`または`->storeAs()`が存在する場合には`NOT NULL`を付与してくれない。この問題を解決するために `->virtualAsNotNull()` および `->storedAsNotNull()` を使えるようにする。

# 実装

コネクションリゾルバの登録方法が5.4から変更されているので注意。[Extending Laravel 5 Blueprint for MySqlConnection](https://gist.github.com/m4grio/acff9ca3da62b6f4317f) の情報は古いです。

（サービスプロバイダの登録は省略）

```php:App/Providers/DatabaseServiceProvider.php
<?php

declare(strict_types=1);

namespace App\Providers;

use App\Database\MySqlConnection;
use Illuminate\Database\Connection;
use Illuminate\Support\ServiceProvider;

class DatabaseServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Connection::resolverFor('mysql', function (...$parameters) {
            return new MySqlConnection(...$parameters);
        });
    }
}
```

```php:App/Database/MySqlConnection.php
<?php

declare(strict_types=1);

namespace App\Database;

use App\Database\Schema\Grammars\MySqlGrammar;
use Illuminate\Database\MySqlConnection as BaseMySqlConnection;

class MySqlConnection extends BaseMySqlConnection
{
    /**
     * Get the default schema grammar instance.
     *
     * @return \Illuminate\Database\Grammar
     */
    protected function getDefaultSchemaGrammar()
    {
        return $this->withTablePrefix(new MySqlGrammar());
    }
}
```

```php:App/Database/Schema/Grammars/MySqlGrammar.php
<?php

declare(strict_types=1);

namespace App\Database\Schema\Grammars;

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Grammars\MySqlGrammar as BaseMySqlGrammar;
use Illuminate\Support\Fluent;

class MySqlGrammar extends BaseMySqlGrammar
{
    public function __construct()
    {
        if (false !== $position = array_search('VirtualAs', $this->modifiers, true)) {
            array_splice($this->modifiers, $position, 0, ['VirtualAsNotNull']);
        }
        if (false !== $position = array_search('StoredAs', $this->modifiers, true)) {
            array_splice($this->modifiers, $position, 0, ['StoredAsNotNull']);
        }
    }

    /**
     * Get the SQL for a generated virtual column modifier with NOT NULL constraint.
     *
     * @param  Blueprint   $blueprint
     * @param  Fluent      $column
     * @return null|string
     */
    protected function modifyVirtualAsNotNull(Blueprint $blueprint, Fluent $column): ?string
    {
        return $column->virtualAsNotNull !== null ? " as ({$column->virtualAsNotNull}) virtual" : null;
    }

    /**
     * Get the SQL for a generated stored column modifier with NOT NULL constraint.
     *
     * @param  Blueprint   $blueprint
     * @param  Fluent      $column
     * @return null|string
     */
    protected function modifyStoredAsNotNull(Blueprint $blueprint, Fluent $column): ?string
    {
        return $column->storedAsNotNull !== null ? " as ({$column->storedAsNotNull}) stored" : null;
    }
}
```