😭

まだ、Laravelのmigrationで疲弊しているの?sqldefを使うだけでmigrationはうまくいく

2022/06/18に公開

お詫び

こんにちは。tyamahoriです。
https://twitter.com/tyamahori

申し訳ございません。タイトルは釣りです。
でも、自分自身は疲弊をしています。。

問題提起

Laravelのmigartion、、皆様どうされてますか?

開発をすすめるごとに増えるmigrationファイル。何も起きないはずがなく。。
もちろん、Laravel8で追加されたスカッシュ機能でいい感じにはなります。。

https://laravel.com/docs/9.x/migrations#squashing-migrations

今回のメイン

そこでおすすめしたいのがsqldefです。
https://github.com/k0kubun/sqldef

The easiest idempotent MySQL/PostgreSQL/SQLite3/SQL Server schema management by SQL.

とありますように、皆様が大好きなPostgreSQLの対応をしています!!!今回はこれをLaravelで使って見たいと思います!

記事を書いたきっかけ

先日こんなツイートをしました。
https://twitter.com/tyamahori/status/1537401494097772544?s=20&t=xinov1CKJsrS2A9CMWbwvg

地味に注目されているようでしたので、今回自分がどのように対応したのかをまとめて行きたいと思ってます。

Laravelで使う

今回は、ローカル環境での利用してみます。
使うのはもちろんみんな大好きDocker/docker composeです。PostgreSQLで試します。MySQLの皆様申し訳ございません。

大まかな手順は以下の通りです。

  1. PHPのdockerイメージに、sqldef, postgresql-client を忍ばせる。
  2. DBに何かしらのスキーマを定義する。
  3. psqldefという、PostgreSQL用のsqldefコマンドを叩く。
  4. 必要に応じて、Artisanコマンドのラッパー処理を書いていい感じに便利にする。

注意点

PostgreSQLの場合、スキーマ名はプロジェクトにおいては統一したほうが良いです。本番環境のスキーマ名はがhoge, ローカルではfugaだと辛くなります。。

Dockerfile

サンプルのDockerfileはこちらです

さんぷるどっかぁふぁいる
FROM composer:2 as build

ENV DOCKERIZE_VERSION=v0.6.1
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
 && tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
 && rm dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz

ENV SQLDEF_VERSION=v0.11.59
RUN /bin/sh -c 'set -ex && ARCH=`uname -m` && \
        if [ "$ARCH" == "aarch64" ]; then \
           echo "$ARCH" && wget https://github.com/k0kubun/sqldef/releases/download/$SQLDEF_VERSION/psqldef_linux_arm64.tar.gz \
            && tar -C /usr/local/bin -xzvf psqldef_linux_arm64.tar.gz \
            && rm psqldef_linux_arm64.tar.gz; \
        else \
           echo "$ARCH" && wget https://github.com/k0kubun/sqldef/releases/download/$SQLDEF_VERSION/psqldef_linux_386.tar.gz \
            && tar -C /usr/local/bin -xzvf psqldef_linux_386.tar.gz \
            && rm psqldef_linux_386.tar.gz; \
        fi'

FROM php:8.1-fpm-alpine3.15

ENV COMPOSER_HOME=/root/composer \
    PATH=$COMPOSER_HOME/vendor/bin:$PATH \
    COMPOSER_ALLOW_SUPERUSER=1 \
    DEBCONF_NOWARNINGS=yes

COPY --from=build /usr/bin/composer /usr/bin/composer
COPY --from=build /usr/local/bin/dockerize /usr/bin/dockerize

COPY --from=node:16-alpine3.13 /usr/local/bin/ /usr/local/bin/
COPY --from=node:16-alpine3.13 /usr/local/lib/node_modules /usr/local/lib/node_modules

COPY --from=build /usr/local/bin/psqldef /usr/bin/psqldef

# php.ini
COPY ./docker/mac/php/php.ini /usr/local/etc/php/php.ini
# php.conf
COPY ./docker/mac/php/zzz-www.conf /usr/local/etc/php-fpm.d/zzz-www.conf

# package install and setup
RUN set -eux \
 && apk add --update-cache --no-cache openssl git autoconf postgresql-dev libtool make gcc g++ libzip-dev libpng-dev libjpeg-turbo-dev freetype-dev python3 postgresql-client \
 && pecl install redis \
 && docker-php-ext-configure gd --with-jpeg=/usr \
 && docker-php-ext-configure opcache --enable-opcache \
 && docker-php-ext-install gd pdo_pgsql pgsql opcache bcmath gd exif zip \
 && docker-php-ext-enable redis \
 && apk del autoconf g++ libtool make && rm -rf /tmp/*

# project
ARG PROJECT_DOMAIN
COPY ./src /opt/$PROJECT_DOMAIN
COPY --chown=www-data:www-data ./src/storage /opt/$PROJECT_DOMAIN/storage

DB周り

何かしらでいいのでテーブルなりを作ります。

スキーマのexport

DBからsqlを作成する場合のコマンドはこちらです。

psqldef -U 'ユーザー名' -h 'DBホスト' 'DB名' -W 'パスワード' --export > '出力するファイルパス'

このコマンドを叩くのでも問題はありませんが、自分はArtisanコマンドでラップします。

さんぷるあるちざんこまんど
<?php

namespace App\Console\Commands;

use Illuminate\Config\Repository;
use Illuminate\Console\Command;
use Illuminate\Foundation\Application;

class SqlDefExport extends Command
{
    /**
     * The name and signature of the console command.
     * @var string
     */
    protected $signature = 'sqldef:export';

    /**
     * The console command description.
     * @var string
     */
    protected $description = 'sqldef wrapper command to export schema.';

    /**
     * Execute the console command.
     * @param Repository $config
     * @param Application $application
     */
    public function handle(
        Repository $config,
        Application $application
    ): void {

        $schemaFile = $application->databasePath() . '/schema.sql';
        $dbUser = $config->get('database.connections.pgsql.username');
        $dbHost = $config->get('database.connections.pgsql.host');
        $dbName = $config->get('database.connections.pgsql.database');
        $dbPassword = $config->get('database.connections.pgsql.password');

        $command = "psqldef -U $dbUser -h $dbHost $dbName -W $dbPassword --export > $schemaFile";

        shell_exec($command);
    }
}

そして、出力されたSQLはこちら

さんぷるえすきゅうえる
CREATE TABLE "tyamahori"."failed_jobs" (
    "id" bigserial NOT NULL,
    "uuid" character varying(255) NOT NULL,
    "connection" text NOT NULL,
    "queue" text NOT NULL,
    "payload" text NOT NULL,
    "exception" text NOT NULL,
    "failed_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY ("id")
);
ALTER TABLE tyamahori.failed_jobs ADD CONSTRAINT failed_jobs_uuid_unique UNIQUE (uuid);

CREATE TABLE "tyamahori"."migrations" (
    "id" serial NOT NULL,
    "migration" character varying(255) NOT NULL,
    "batch" integer NOT NULL,
    PRIMARY KEY ("id")
);

CREATE TABLE "tyamahori"."password_resets" (
    "email" character varying(255) NOT NULL,
    "token" character varying(255) NOT NULL,
    "created_at" timestamp
);
CREATE INDEX password_resets_email_index ON tyamahori.password_resets USING btree (email);

CREATE TABLE "tyamahori"."personal_access_tokens" (
    "id" bigserial NOT NULL,
    "tokenable_type" character varying(255) NOT NULL,
    "tokenable_id" bigint NOT NULL,
    "name" character varying(255) NOT NULL,
    "token" character varying(64) NOT NULL,
    "abilities" text,
    "last_used_at" timestamp,
    "created_at" timestamp,
    "updated_at" timestamp,
    PRIMARY KEY ("id")
);
CREATE INDEX personal_access_tokens_tokenable_type_tokenable_id_index ON tyamahori.personal_access_tokens USING btree (tokenable_type, tokenable_id);
ALTER TABLE tyamahori.personal_access_tokens ADD CONSTRAINT personal_access_tokens_token_unique UNIQUE (token);

CREATE TABLE "tyamahori"."users" (
    "id" bigserial NOT NULL,
    "name" character varying(255) NOT NULL,
    "email" character varying(255) NOT NULL,
    "email_verified_at" timestamp,
    "password" character varying(255) NOT NULL,
    "remember_token" character varying(100),
    "created_at" timestamp,
    "updated_at" timestamp,
    PRIMARY KEY ("id")
);
ALTER TABLE tyamahori.users ADD CONSTRAINT users_email_unique UNIQUE (email);

スキーマの反映

先程出力されたSQLをDBに反映させます。
まず一旦はDBを消します。

./artisan db:wipe

このコマンドでまずはDBを消します。

そしてpsqldefコマンドを叩いてDBにスキーマ情報を反映させます。
公式ドキュメントを見てpsqldefコマンドを叩くのも良いですが、自分はArtisanコマンドにラップして使いたいと思います。

さんぷるあるちざんこまんど
<?php

namespace App\Console\Commands;

use Illuminate\Config\Repository;
use Illuminate\Console\Command;
use Illuminate\Foundation\Application;

class SqlDefExecute extends Command
{
    /**
     * The name and signature of the console command.
     * @var string
     */
    protected $signature = 'sqldef:execute';

    /**
     * The console command description.
     * @var string
     */
    protected $description = 'sqldef wrapper command to export schema.';

    /**
     * Execute the console command.
     * @param Repository $config
     * @param Application $application
     */
    public function handle(
        Repository $config,
        Application $application
    ): void {

        $environment = $application->environment();

        if ($environment === 'testing') {
            $this->error('This command is not available in testing environment.');
            return;
        }

        $schemaFile = database_path() . '/schema.sql';
        $dbUser = $config->get('database.connections.pgsql.username');
        $dbHost = $config->get('database.connections.pgsql.host');
        $dbName = $config->get('database.connections.pgsql.database');
        $dbPassword = $config->get('database.connections.pgsql.password');

        $dryRunCommand = "psqldef -U $dbUser -h $dbHost $dbName -W $dbPassword --dry-run < $schemaFile";
        $dryRunCommandOutPut = shell_exec($dryRunCommand);
        if (!$dryRunCommandOutPut) {
            $this->info('Failed to execute command.');
            return;
        }
        $this->info($dryRunCommandOutPut);
        if (!$this->confirm("You are in【'$environment'】. Do you wish to continue previous outputs ↑ ? :")) {
            $this->info('Aborted.');
        }

        $commandToExecute = "psqldef -U $dbUser -h $dbHost $dbName -W $dbPassword < $schemaFile";
        $commandToDisplay = 'psqldef -U $dbUser -h $dbHost $dbName -W $dbPassword < $schemaFile';

        $this->info("Executing command: $commandToDisplay");
        shell_exec($commandToExecute);
        $this->info('Command executed.');
    }
}

スキーマの更新

先程出力されたSQLをベースにカラムを消したり、追加したり、外部キー制約を貼ったりしてみてください。その後、sqldefコマンドを叩くと、それららの差分だけがいい感じに更新されるかなと思います!

その他考慮すること

自分はこのsqldefを気に入っています。しかしながらチーム開発においては以下のことを考慮するべきかなと思っています。

  • 本当にLaravel migrationから脱却するかの議論
  • sqldefを使う場合の運用方針について
  • テストにおいてRefreshDatabaseを使っている場合は、その代替手段の検討

自分がぱっと思いつくものを上げてみました。

最後に

いかがでしたか?皆様に有益な情報であることを願っています。

参考リンク

tyamahoriのサンプルリポジトリ。今回の内容を格納しています。
https://github.com/tyamahori/laravel-template

sqldef GitHub repository
https://github.com/k0kubun/sqldef

Discussion