Laravel × Dacapo で始める快適マイグレーション生活！

新しくv4.0をリリースしたので、解説記事をリニューアルしました！
[Laravel × Dacapo データベースマイグレーションサポートツールの使い方](https://qiita.com/ucan-lab/items/e75ef2bba744d9dcd4d0)

[Laravel Advent Calendar 2019 - Qiita](https://qiita.com/advent-calendar/2019/laravel) の 1日目 の記事です。

## 去年(Laravel Advent Calendar 2018)の記事

- [Laravel 整理整頓したマイグレーションファイルを生成するライブラリを作った話](https://qiita.com/ucan-lab/items/10bb60e10c5b26a59b92)

実際に弊社の実務でDacapoを運用し、他の会社さんから使ってもらったフィードバックを活かして去年より大幅アップデートしてます。

## リポジトリ

https://github.com/ucan-lab/laravel-dacapo

## 何をするツールか

Laravelのマイグレーションファイル生成をサポートするライブラリです。

## 困っていること

- 開発中はテーブル構成を変更する頻度が高い
- 不要なマイグレーションファイルが増える
- 似た構成のテーブルをコピペして作るにも変更箇所が多い
  - ファイル名、クラス名、テーブル名を直すのがめんどう...
  - 手動で直すとオートローダーがバグる
- 最新のテーブル構成は実際のDBを見ないと分からない
- インデックスや外部キーが入るとさらにややこしく...

## ダカーポ開発の流れ

- `database/schemas/*.yml` テーブル定義をymlファイルに記述する
  - 「スキーマ」と呼びます。
- `php artisan dacapo` マイグレーションファイルを生成&マイグレーション実行

スキーマを変更した場合は必ず、 `php artisan dacapo` して、マイグレーションファイルを生成し直す流れになります。

## ダカーポの注意・留意事項

- 特性上、プロジェクトの初期フェーズに使用するライブラリです。
- 運用フェーズに入ったらダカーポはアンインストールし、通常のマイグレーション運用に戻します。
- 本番のデータが全部消えてしまうので...。。

- schema.yml の記述を元に常に最新のマイグレーションファイルを生成します。(上書き)
- `php artisan migrate:fresh` で全テーブルの削除し、マイグレーションを実行する運用です。

## 生成されるマイグレーションファイルの種類

- `1970_01_01_000000_*.php` テーブル作成のマイグレーションファイル
- `1970_01_01_000001_*.php` 一意制約、インデックスのマイグレーションファイル
- `1970_01_01_000002_*.php` 外部キー制約のマイグレーションファイル

マイグレーションファイルが `database/migrations` に出力されます。
マイグレーションはファイル名順に実行されます。
テーブルを作成したあとに制約を貼るようにしています。

整理整頓されたマイグレーションファイルが出力されます☺️

## インストール手順

```shell-session
$ composer require --dev ucan-lab/laravel-dacapo
```

## Laravelの初期マイグレーションを生成(任意)

Laravelでは、標準で下記の3テーブルのマイグレーションが用意されています。

- users
- password_resets
- failed_jobs

これと同じ構成のスキーマファイルを生成するダカーポコマンドを用意しました。

## dacapo:init コマンド

```shell-session
$ php artisan dacapo:init
```

`database/schemas/default.yml` ファイルが生成されます。

```yaml
users:
  columns:
    id: bigIncrements
    name: string
    email:
      type: string
      unique:
    email_verified_at:
      type: timestamp
      nullable:
    password: string
    rememberToken:
    timestamps:

password_resets:
  columns:
    email:
      type: string
      index:
    token: string
    created_at:
      type: timestamp
      nullable:

failed_jobs:
  columns:
    id:
    uuid:
      type: string
      unique:
    connection: text
    queue: text
    payload: longText
    exception: longText
    failed_at:
      type: timestamp
      useCurrent:

personal_access_tokens:
  columns:
    id:
    tokenable: morphs
    name: string
    token:
      type: string
      args: 64
      unique:
    abilities:
      type: text
      nullable:
    last_used_at:
      type: timestamp
      nullable
    timestamps:
```

## dacap コマンド

先ほどの `dacapo:init` で生成した `default.yml` がある状態で `dacapo` を実行します。

```shell-session
$ php artisan dacapo
```

`1970_01_01_000000_create_users_table.php`

```php
<?php

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

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    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();
        });
    }

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

`1970_01_01_000000_create_password_resets_table.php`

```php
<?php

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

class CreatePasswordResetsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('password_resets', function (Blueprint $table) {
            $table->string('email')->index();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });
    }

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

`1970_01_01_000000_create_failed_jobs_table.php`

```php
<?php

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

class CreateFailedJobsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('failed_jobs', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->text('connection');
            $table->text('queue');
            $table->longText('payload');
            $table->longText('exception');
            $table->timestamp('failed_at')->useCurrent();
        });
    }

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

このように最小限のテーブル構成をymlに記述すればマイグレーションファイルを生成してくれます。

### dacapo オプション

```shell-session
# seedオプション
$ php artisan dacapo --seed
# no-migrateオプション
$ php artisan dacapo --no-migrate
```

マイグレーション生成＆マイグレーション実行＆シーディング実行をオプション付けて一緒に実行できます。

## スキーマの書き方

- `database/schemas/*.yml` と配置します。
- 複数ファイルを配置できます。

### スキーマのフォーマット

```yaml
テーブル名:
  columns:
    カラム名: カラムタイプ
    カラム名:
      type: カラムタイプ
      args: カラムタイプの引数
```

### カラムタイプの指定

- [カラム | 
Laravel 6.x データベース：マイグレーション](https://readouble.com/laravel/6.x/ja/migrations.html#creating-columns)

テーブルを構築する時に使用する様々なカラムタイプを指定できます。

```yaml
how_to_column_types:
  columns:
    id: bigIncrements
    name1: string
    name2:
      type: string
    name3:
      type: string
      args: 100
    name4:
      type: string
      args: [100]
    amount1: decimal
    amount2:
      type: decimal
    amount3:
      type: decimal
      args: 8
    amount4:
      type: decimal
      args: [8]
    amount5:
      type: decimal
      args: [8, 2]
    level:
      type: enum
      args: ['easy', 'hard']
    timestamps:
    # timestamps: 0
    # timestamps: [0]
    softDeletes:
    # softDeletes: 'deleted_at'
    # softDeletes: ['deleted_at']
    # softDeletes: ['deleted_at', 0]
```

`database/migrations/1970_01_01_000000_create_how_to_column_types_table.php`

```php
    public function up()
    {
        Schema::create('how_to_column_types', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name1');
            $table->string('name2');
            $table->string('name3', 100);
            $table->string('name4', 100);
            $table->decimal('amount1');
            $table->decimal('amount2');
            $table->decimal('amount3', 8);
            $table->decimal('amount4', 8);
            $table->decimal('amount5', 8, 2);
            $table->enum('level', ['easy', 'hard']);
            $table->timestamps();
            // $table->timestamps(0); // timestamps: 0
            // $table->timestamps(0); // timestamps: [0]
            $table->softDeletes();
            // $table->softDeletes('deleted_at'); // softDeletes: 'deleted_at'
            // $table->softDeletes('deleted_at'); // softDeletes: ['deleted_at']
            // $table->softDeletes('deleted_at', 0); // softDeletes: ['deleted_at', 0]
        });
    }
```

### カラム修飾子

[カラム修飾子 | 
Laravel 6.x データベース：マイグレーション](https://readouble.com/laravel/6.x/ja/migrations.html#column-modifiers)

上記のカラムタイプに付け加え、カラムを追加するときに使用できる様々な修飾子もあります。たとえばカラムを「NULL値設定可能(`nullable`)」にしたい場合は、`nullable`メソッドを使います。

また、ダカーポの特性上 `after`, `first` のカラム修飾子は使用できません。

```yaml
how_to_column_modifiers:
  columns:
    id:
      type: integer
      autoIncrement: true
    name:
      type: string
      charset: utf8
      collation: utf8_unicode_ci
      comment: my comment
      default: test value
      nullable: true
    price:
      type: integer
      unsigned: true
    count:
      type: integer
      unsigned: true
    total_stored:
      type: integer
      storedAs: price * count
    total_virtual:
      type: integer
      virtualAs: price * count
    total_generated:
      type: integer
      generatedAs: price * count
    created_at:
      type: timestamp
      useCurrent: true
    seq:
      type: integer
      always: true

```

`database/migrations/1970_01_01_000000_create_how_to_column_modifiers_table.php`

```php
        Schema::create('how_to_column_modifiers', function (Blueprint $table) {
            $table->integer('id')->autoIncrement();
            $table->string('name')->charset('utf8')->collation('utf8_unicode_ci')->comment('my comment')->default('test value')->nullable();
            $table->integer('price')->unsigned();
            $table->integer('count')->unsigned();
            $table->integer('total_stored')->storedAs('price * count');
            $table->integer('total_virtual')->virtualAs('price * count');
            $table->integer('total_generated')->generatedAs('price * count');
            $table->timestamp('created_at')->useCurrent();
            $table->integer('seq')->always();
        });
```

## インデックス

```yaml
users1:
  indexes:
    email_index:
      columns: name
      type: index
  columns:
    id: bigIncrements
    name:
      type: string
    email:
      type: string

users2:
  indexes:
    email_index:
      columns:
        - name
        - email
      type: index
      alias: users_name_index
  columns:
    id: bigIncrements
    name:
      type: string
    email:
      type: string

users3:
  indexes:
    email_index:
      columns:
        - name
        - email
      type: index
      alias: custom_index
  columns:
    id: bigIncrements
    name:
      type: string
    email:
      type: string
```

実際に出力されるファイル:
https://github.com/ucan-lab/laravel-dacapo/tree/master/tests/Storage/index

## 一意制約

```yaml
users1:
  indexes:
    email_index:
      columns: name
      type: unique
  columns:
    id: bigIncrements
    name:
      type: string
    email:
      type: string

users2:
  indexes:
    email_index:
      columns:
        - name
        - email
      type: unique
      alias: users_name_unique_index
  columns:
    id: bigIncrements
    name:
      type: string
    email:
      type: string

users3:
  indexes:
    email_index:
      columns:
        - name
        - email
      type: unique
      alias: custom_index
  columns:
    id: bigIncrements
    name:
      type: string
    email:
      type: string
```

実際に出力されるファイル:
https://github.com/ucan-lab/laravel-dacapo/tree/master/tests/Storage/unique_index

## 外部キー制約

```yaml
users:
  columns:
    id: bigIncrements
    name: string

tags:
  columns:
    id: bigIncrements
    title: string

tasks:
  relations:
    - foreign: user_id
      references: id
      on: users
      onUpdate: cascade
      onDelete: cascade
  columns:
    id: bigIncrements
    user_id: unsignedBigInteger
    content: string

task_tag:
  relations:
    - name: task_tag_custom_task_id_foreign
      foreign: task_id
      references: id
      on: tasks
    - foreign: tag_id
      references: id
      on: tags
  columns:
    task_id: unsignedBigInteger
    tag_id: unsignedBigInteger
```

実際に出力されるファイル:
https://github.com/ucan-lab/laravel-dacapo/tree/master/tests/Storage/relation/migrations

## ダカーポのアンインストール

開発時は全テーブルドロップしてマイグレーション、シーディング実行で問題ありませんが、
運用開始してしまうと、本番テーブルをドロップする訳にはいきません...

運用フェーズに移行したらダカーポを削除して通常のマイグレーション運用に戻しましょう。

```
$ php artisan dacapo:uninstall
$ composer remove --dev ucan-lab/laravel-dacapo
```

## Dacapo (ダカーポ) の由来

ライブラリ名の由来ですが、演奏記号の[ダ・カーポ](https://ja.wikipedia.org/wiki/%E3%83%80%E3%83%BB%E3%82%AB%E3%83%BC%E3%83%9D_(%E6%BC%94%E5%A5%8F%E8%A8%98%E5%8F%B7))から命名しました。

ダカーポは「最初に戻って繰り返す。」という意味があります。

PHPのパッケージ管理ツール[Composer](https://getcomposer.org)が作曲家という意味なので音楽用語でこれだと思った名前に決めました！

## 着想

ダカーポライブラリの着想は [Laravel/Vue.js勉強会#3 - connpass](https://laravue.connpass.com/event/75495) に参加して @mpyw さんの[LT](https://qiita.com/mpyw/items/5c1943ed9edd381fe50d)に影響を受けて作成しました。 
また、Symfony1.4系の頃にあったDoctrineのDBスキーマ定義も参考にしてます。

- [Laravel/Vue.js勉強会#3 - connpass](https://laravue.connpass.com/event/75495)
- [Laravelお役立ちネタ6連発](https://qiita.com/mpyw/items/5c1943ed9edd381fe50d)
- [symfony schema.yml の覚書](https://vivid-skid.hatenadiary.org/entry/20120127/1327650268)

**Laravel × Dacapo で快適なマイグレーション生活を！**