読者です 読者をやめる 読者になる 読者になる

Goでのmigrationについて ~ ddl-maker × schemalex ~

AdventCalendar2016 golang mysql

この記事はカヤックアドベントカレンダー19日目の記事です。

はじめに

こんにちは @Konboi です。

皆さん忘年してますか? 私は今のところ順調に忘年できており、今年何をしてたのか大分忘れました。

ちなみに先日行われたカヤック技術部の忘年会の様子です。

ということで表題の通り、今日の記事ではカヤックでのこれからのGoプロジェクトでのmigrationについて紹介しようと思います。

本記事であつかう migrationはDB更新を差し

  • 新規テーブルの作成
  • テーブル/カラム名の変更
  • テーブルへのカラムの追加/削除
  • テーブルへのindexの追加/削除

などがあげられます。

TL;TR

migrationの方法

migrationの方法については大きく分けて3つあると思います (細かい所ではもっと分かれるとは思います。 異論は認めます)

  1. 更新分をSQLファイルで追加していく方法
  2. Rails like な DSLを記述し差分をファイルとして追加していく方法
  3. 現在のDBの状況と定義ファイルの差分を適用する方法

更新分をSQLファイルで追加していく方法

  • ベースとなるSQLファイル (サービスリリース時点のSQL) が存在する
  • DB更新時には更新分のSQLを作成し、 ベースファイルに更新を適用する
  • 本番ヘは、更新分のSQLのみ適用する
  • 新しく入ってきたメンバーはベースファイルを適用し、更新分を範囲していく

メリット

  • 更新分がわかりやすい
  • 書かれたSQLが適用されるのでレビューがし易い
  • migration実行時に別途ツールが必要ない

デメリット

  • 管理がベースファイルと新規ファイル両方に適用しないと行けないので煩雑

※ 個人の意見です

rails likeなmigration方式

  • 基本的な方針はSQLを追加してく方式と同じ
  • SQLファイルを追加していくのではなくDSLで定義されたファイルを追記していく
  • 定義ファイルを作成/編集 (rails g migration <Name>)
  • 定義ファイルを反映 (rails db:migrate)

メリット

  • アプリケーション開発と同じ言語で書ける
  • 反映分を切り戻す戻すことも可能 (rails db:rollback)

デメリット

  • DSLを覚える必要がある
  • 複数人で開発しているとコンフリクトする
  • どのようなSQLが吐かれるかをきちんと確認する必要がある

※ 個人の意見です

現在のDBの状況と定義ファイルの差分を適用する方法

カヤックのゲーム事業部ではこの方法でmigrationを行っています。

GitDDLというツールを使用しています。

  • その名の通りgitで管理されていることが前提
  • 反映時のコミットハッシュをdatabase上のテーブルで管理
  • 現在のschemaファイルとdbに保存されているコミットハッシュ時のschemaの状態から差分を生成
  • 差分を適用し、DBに保存されているコミットハッシュを更新

GitDDL以外で似たようなツールには Anego があげらると思います。

Perl以外ではridgepole などがあります。

メリット

  • 管理がし易い
  • コンフリクトが起きにくい

デメリット

  • 差分のSQLを確認する必要がある
  • 定義ファイルを何かしらの方法で管理する必要がある
  • どのようなSQLが吐かれるかをきちんと確認する必要がある

Goでのmigrationの話

長々と既存のmigrationの話をしましたが、Goの話をします。

Go製マイグレーションツールの現状確認Githubで調べてみるとGoではrails like なDBのmigrationが多いように見受けられます。

しかし、僕たちはGitDDLを使った運用が長くその形での運用ノウハウが多く溜まっているので、GoのプロジェクトでもGitDDLの様な差分を元にmigrationを実行したいと考えていました。

そこで目をつけたのが schemalex です

github.com

schemalexはREADME.mdを見てもらえるとわかりますが、 SQL::Translator::Diff のように2つのSchemaの差分からDiffを生成してくれるツールです。

ある程度複雑な差分を与えても問題なく動いています。

GoのstructからSQLを自動生成する

既存のPerlプロジェクトではGitDDLで使用するSchemaファイルを自動生成*1しています

Goのプロジェクトでも適用するSQLファイルを自動生成したい要望があがりました。

そこで作ったのが ddl-maker です。

github.com

ddl-makerはGoのstructからSQLを生成してくれます。 詳しい使い方は README.md や example をみていただければと思いますが簡単な概要を

type User struct {
    Id        uint64
    Name      string
    CreatedAt time.Time
    UpdatedAt time.Time
}

func (u User) PrimaryKey() dialect.PrimaryKey {
    return mysql.AddPrimaryKey("id")
}

このような struct を ddl-maker を介すと以下の様なSQLを生成することができます

SET foreign_key_checks=0;

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
    `id` BIGINT unsigned NOT NULL,
    `name` VARCHAR(191) NOT NULL,
    `created_at` DATETIME NOT NULL,
    `updated_at` DATETIME NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;

SET foreign_key_checks=1;

以下の様なメソッドを定義してあげることでテーブル名の指定しての生成ややインデックスやプライマリーキーも生成することができます。

tagではなく、メソッドにしているのはPrimaryKeyなどはパーティションを切るときなどきちんと順番を指定したい場合があるためです

func (u User) Table() string {
    return "player"
}

func (u User) Indexes() dialect.Indexes {
    return dialect.Indexes{
        mysql.AddIndex("name_idx", "name"),
    }
}
CREATE TABLE `player` (
    `id` BIGINT unsigned NOT NULL,
    `name` VARCHAR(191) NOT NULL,
    `created_at` DATETIME NOT NULL,
    `updated_at` DATETIME NOT NULL,
    INDEX `name_idx` (`name`),
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;

これで、Goの開発においては

  1. struct を定義する
  2. ddl-makerでeructからSQLを生成
  3. schemalexでDBに反映

という流れができました。

最後に

各社migrationについて色々と思うところはあると思います。 今回の記事が少しでも参考にされば幸いです。

明日は 今年のISUCON6で準優勝したmorimoto組組長こと@jinnです

きっと凄いのがくると思います!! お楽しみに!!!

おまけ

カヤックでは美味しいビールと美味しいお肉で忘年したいと人を募集しています

入社はしないけど、ビールとお肉片手にカヤック社員とGo談義したい人も募集しております。 話したい/飲みたいというかたは @Konboi まで気軽にmention下さい!

みんなのGo言語【現場で使える実践テクニック】

みんなのGo言語【現場で使える実践テクニック】

*1:DBIx::Class + GitDDLでなんかそれっぽいSQL出す。 http://hisaichi5518.hatenablog.jp/entry/2013/08/12/182356