Ruby
Go
MySQL
docker

RubyistがGoに入門してDBマイグレーションツールを作った

More than 1 year has passed since last update.

普段Rubyやjsを書いてる自分がGo言語でツールを作ってみました。
コードを書くにあたって、ハマったところやいいなと思ったところ、使ったライブラリ等をツラツラと書いてこうかなと思います。

SQLべた書きマイグレーションツール(going)
https://github.com/Islands5/going

きっかけ

2016年、年明け
Dockerを少しかじり始めた僕は、dockerがGoで書かれていることを知って
『今年はGo言語勉強するぞー』
と心の中で誓いました。

ゴールデンウィークにやるからいいやー
お盆にやるからいいやー
シルバーウィークにや..( ゚д.∵:・..

...いつのまにか11ヶ月と少し過ぎ
気づけば、今年はあと一ヶ月もありません

さあ、そろそろ体重三桁の重い腰を上げる時が来たようです。

今年も弊社ではアドベントカレンダーに参加するということだったので、ポチッと参加してみました。
ADD(アドベント駆動開発)です。(最近弊社では〜駆動開発というワードが流行っています)

DBマイグレーションツール

DBマイグレーションツールは、DBに保存されているデータを保持したまま、テーブルの作成やカラムの変更を行うツールです。
普段自分はRailsでRDBを使っている方は、bundle exec rake db:migrateとか打ったりしてると思うのですが、今回作成するのはRailsのmigrateのようにDSL書いたりとか複雑なものではありません。

基本的な仕様は以下です。

  • init: 設定ファイル等を作成
  • up: SQLを順番に実行
    ※ 一度実行されたものは実行されない
  • reset: DBを削除して新しいDBを作成

まずは下準備(Go環境の作成)

homebrewでGoをサッと入れました。(Go 1.7.1)
使い始めるにあたって、環境変数の設定等は必要であることがわかったので、以下のサイトを参考に
http://golang-jp.org/doc/code.html#GOPATH
環境変数を設定

これで準備が整いました。

サブコマンドの導入

今回作成するツール(going)はサブコマンドを持つツールです。
例えば

$ going init
$ going up
$ going reset

みたいな使い方をします。

このようなサブコマンドを実現するために
googleが出してるサブコマンドライブラリ
https://github.com/google/subcommands
を使用しました。

This is not an official Google product

と書いてるのですが、googleってネームスペースでコードが上がってるので絶対大丈夫だろ!なんて思いながら導入しました。

コマンドの実装

initコマンド

サブコマンドライブラリの使い方はなんとなく理解したので、次はコマンドの実装に移ります。
使う順番で実装していこうと思ったので、まずinit
このコマンドでは、マイグレーションに必要な設定ファイルを作成します。

最初はひとつずつファイルやディレクトリを作成していこうかな〜って考えてました。
しかし、変更とか追加のことを考えるとテンプレ用意してコピーする方がいいなと思ったので、assetsというディレクトリをテンプレにして、コマンドを実行したディレクトリにgoing-assetsという設定用のディレクトリを作成するようにしました。

exec.Command("cp", "-r", path, "./going-assets").Run()

この最後についてるRun()を忘れていて、まあまあ悩みました。
自分でかけば済む話なのですが、再帰的なコピーを行うコマンドがGoにはないっぽいです。(もしありましたら教えてくださいm(_ _)m)

upコマンド

主役的なコマンドです。
このコマンドではinitで作成したgoing-assets/sqlの中に作成した.sqlファイルを読み込んでデータベースに適用していきます。

sqlファイルの命名規則は

V1__unknown_file.sql

のようにバージョンとアンスコ×2が付いてればどんな名前でもOK
実行されたsqlはgoing-assets/.goingにV1__YearMonthDateHourMiniteSecondで保存されていきます。(このファイルをチェックして適用済みか判断します)

そんな感じの仕様で実装に取り掛かりました。

設定ファイルはymlで記述します。

"gopkg.in/yaml.v2"
をインポートするとyaml.Unmarshalが使えるようになって、ファイルから読み込んだバイト文字列を使いやすい形に吐き出してくれます。

buf, err := ioutil.ReadFile(filename)
if err != nil {
  fmt.Println(err)
}

connInfo := make(map[interface{}]interface{})
err = yaml.Unmarshal(buf, &connInfo)
if err != nil {
  fmt.Println(err)
}

GoではファイルI/Oのパッケージがいくつかあって、それの違いを理解するのに苦労しました。
参考にしたのは

逆引きGo
はじめてのGo言語

データベースの接続も意外とすんなりいきました。
使ったライブラリは

https://github.com/go-sql-driver/mysql

検索したら一番上に出てきたので、導入。
Wikiを確認したら詳しくサンプルが載っていたので、スムーズにDB接続が行えました。

"database/sql"
_ "github.com/go-sql-driver/mysql"
をインポートすれば、

conn := fmt.Sprintf("%s:%s@/%s", user, password, database)
db, err := sql.Open("mysql", conn)

これで接続完了
[]byteとstringの壁にぶつかったりしたのですが、Sprintf使えばほとんど大丈夫というコメントを見つけ、愚直に信じて突き進みました。
(参考にしたサイトを見つけられませんでした)

db.Query(string(buf))
クエリストリングを流すだけでファイル内のSQLが実行されます。

で、実行した後の*.sqlファイルのバージョンを目印として.goingファイルに書き出します。
形式はV1__西暦年月日時分のように決めました。

Rubyだと %Y%m%d みたいにタイムフォーマットを書くのですが
Goは2006010203

_人人 人人_ > 突然の2006 <  ̄Y^Y^Y^Y ̄

疲れてるのかなって思いました。

この形式の理由はQiitaで@ruiuさんが記述されていて、

これはアメリカ式の時刻の順番なのだ。"1月2日午後3時4分5秒2006年"

引用元

ということです。

あとは実行したいSQLが、すでに実行済みか.goingに書き込まれているバージョンをチェックする処理です。
正規表現の時間がやってきました。

Rubyのときに正規表現を書く際使っているツールなんですが、
http://rubular.com
こちらで実際にマッチするかどうかを確認しながら作業すれば、すんなり完了。

ほぼ何も出力しない、職人肌のコマンドが出来上がりました。
ただひたすらSQLを実行してくれます。

resetコマンド

データベースを削除、.goingを削除して新しい.goingファイルを作成するだけ!

ひっかかったこと

バージョン

subcommandsのサンプルコードをコピーして動かそうと思ったのですが、動かない
コピーしてきたのでタイポとかないし、なんでだろと思ったのですが原因はGoのバージョンでした。

import (
  "flag"
  "fmt"
  "os"
  "strings"

  "github.com/google/subcommands"
  "golang.org/x/net/context"
  # "context"
)

サンプルコードでは上記のインポートを行っていたのですが、Go1.7系だとcontextパッケージがすでに組み込まれているということで上手く動作しなかったようです。
"golang.org/x/net/context"の変わりに"context"を入れると動作しました。

これに一番最初にひっかかって、そこそこの時間悩んでしまいました。。。

変数の宣言

hoge, err := greatFunction(piyo)
err := superFunction(mega) #=> エラーになる

hoge, err := greatFunction(giga)
poke, err := superFunction(mega) #=> エラーにならない

よくよく考えれば当然か〜なんて思うのですが、上のケースはerrを2回宣言してることになるのでエラーになるようです。
ただ、左辺に新しく宣言する変数(上だとpoke)があればエラーにならないという。。。

良くも悪くも

型です。
作業の途中に口うるさく言われている気持ちでした。
ただ、見守られている安心感からか、コマンドを実行する時もエンターキーを押す強さが半減したような気がします。
バイト列とstring、よしなにやってくれないかな〜なんて思ったり思わなかったり。

あと本来はもっと型宣言を関数の上の方に書いたりしないといけないと思うのですが、:=に甘えてしまってゴリゴリに使ってしまいました。

これからやりたいこと

僕は型付きの言語をちゃんとやったことがないので、これを機会にさらに深めて行きたいなと思います。
近日中に行うことは

  • テストコードの追加
  • ファイル分割

です。
ファイルの分割は、チャレンジしたのですが上手くいかなかったです...
コマンドごとにファイル切り分けたかった(泣)

参考

おまけ(goingの使い方)

動作確認環境

MacOS Sierra
Go1.7.1(1.7未満だと動かないかも...)

インストール

$go get github.com/islands5/going

これでgoingコマンドが使えるようになります。

データベースの準備

mysqlがPCに入っている方は、ターゲットのデータベースが作成されているか確認して次のステップへお進みください
mysqlがPCに入ってない方は、docker-composeを使って立ち上げます。

Docker for Mac
こちらをインストールしていただいて、

$cp -r $GOPATH/src/github.com/islands5/going/sample ./
$cd sample
$docker-compose up

#別のターミナルを立ち上げる
$docker exec -it sample_db /bin/bash
#=>dockerコンテナの中へ
$mysql -u root -ppassword
#CREATE DATABASE going_demo

これでテスト環境ができました

初期化

どこでもいいので作業ディレクトリへ移動して

$going init

を実行してください
カレントディレクトリにgoing-assetsというディレクトリが作成されます。

設定&SQLの記述、反映

going-assets/going.ymlにデータベースの設定を記述します。
/sql内に実行したい*.sqlファイルを命名規則に従って配置していきます。
規則は
V{バージョン名}__{適当な名前}.sql
サンプルのファイルは

V1__create_person_table.sql

という風になっています

コマンドを実行してみます。

$going up
#=> applying: V1__create_person_table.sql...

これでDBに反映されます。

続いて、Siteテーブルを追加します。
例によってgoing-assets/sql/V2__create_site_table.sqlを作成します。

$going up
#=> already applied: V1__create_person_table.sql
applying: V2__create_site_table.sql...


データベース
+----------------------+
| Tables_in_going_demo |
+----------------------+
| person               |
| site                 |
+----------------------+
2 rows in set (0.01 sec)

削除!

$going reset

!!使うときはデータベースが跡形もなく消えるので気をつけてください