Help us understand the problem. What is going on with this article?

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

Laravel Advent Calendar 2019 - Qiita の 1日目 の記事です。

去年(Laravel Advent Calendar 2018)の記事

実際に弊社の実務で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 に出力されます。
マイグレーションはファイル名順に実行されます。
テーブルを作成したあとに制約を貼るようにしています。

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

インストール手順

$ composer require --dev ucan-lab/laravel-dacapo

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

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

  • users
  • password_resets
  • failed_jobs

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

dacapo:init コマンド

$ php artisan dacapo:init

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

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

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

failed_jobs:
  columns:
    id: bigIncrements
    connection: text
    queue: text
    payload: longText
    exception: longText
    failed_at:
      type: timestamp
      useCurrent: true

dacapo:init オプション

また、Laravelのバージョンによって初期マイグレーションの構成が異なるので、オプションを用意しています。

# Laravel 5.7 〜 5.8
$ php artisan dacapo:init --laravel57

# Laravel 5.0 〜 5.6
$ php artisan dacapo:init --laravel50

dacap コマンド

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

$ php artisan dacapo

1970_01_01_000000_create_users_table.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

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

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 オプション

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

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

スキーマの書き方

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

スキーマのフォーマット

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

カラムタイプの指定

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

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

    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 データベース:マイグレーション

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

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

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

        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();
        });

インデックス

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

一意制約

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

外部キー制約

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 (ダカーポ) の由来

ライブラリ名の由来ですが、演奏記号のダ・カーポから命名しました。

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

PHPのパッケージ管理ツールComposerが作曲家という意味なので音楽用語でこれだと思った名前に決めました!

着想

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

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

ucan-lab
Backend Developer at ROLO. I love PHP and I'm focusing on Laravel, Docker, GraphQL.
https://u-can.pro
miraito-inc
システムデザインを中心に置いた開発により高品質で使いやすいシステムを提供いたします。業務システム構築、アプリ開発、コンサルティングまで幅広く手がけています。
https://miraito-inc.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした