GoでPostgreSQLのDB初期化ローダー作ってみた

# はじめに
本記事はGoでWebアプリケーション開発にあたって、データストアであるDBを初期化するGoパッケージを作ったので、その内容のまとめです。
仕事だとサーバ側はJavaなのでDBの初期化はGradleやらでやるんですが、Goだとどうやるんだろう？ということで作ってみた。
ここでいうDBの初期化とは、アプリケーションが使うテーブルだったりパッケージだったり初期データだったりをコマンドラインから一発でDBに作るための何某かのプログラムを指しています。
CICDなんてかっこいい言葉を使わずとも本番、テスト環境等の構築で必ず作ることになると思うのですが、GradleやらAntやらのビルドツールが豊富なJavaと違ってGoでそういったツールの存在を知らないので、じゃあちょっと作ってみようぜ！という記事になります。

## ソースコードと注意

この記事自体が以前投稿した[Go＋AngularでWebアプリ開発](https://qiita.com/ponta-dev/items/7ac092c427d855d3153e)のサーバ側構築の事前準備的なポジションなので、同じ[Githubリポジトリ](https://github.com/ponta-dev/angular-go/tree/master/loader)に置いてあります。
また本来は接続先のDBのIPやらユーザ、DB名やらのDB接続文字列に使うような情報や、ローダーで実行するSQLファイルの指定等は設定ファイルとして外だししておくのがセオリーな気がしますが、今回の実装ではそこまではやっていません。
後々時間ができたら外だし予定。。

# ローダーの構成

ここから作ったローダーの説明をしていきます。
作ったGoのローダーの構成は下記の感じ。
![WS000003.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445416/d7af0bb9-3633-3d6d-1d20-ea77618b2ade.jpeg)

sqlディレクトリ以下にCREATEやDROP、DMLなどのSQLファイルを配備します。
go build ./loaderでビルドしたのち、loader.exeをたたくとローダーが起動します。
gitignoreで除外しているsetting_secret.goはDBの接続情報とかの定数を抱えています。（ここがそのうち設定ファイルにかわる予定）

今回作るDBのオブジェクトはUSERSテーブルとメモテーブルの2つです。（超シンプル！）
SQLファイル自体は非常にシンプルなただのCREATE文とかで、ローダは起動時のパラメータによってDBにテーブルだけ作ったり、ついでに初期データも作ったり、あるいはデータだけクリアしたりします。

# ローダーの起動方法について

試しにloader.exeをキックすると下記のようなコンソール出力が出ます。

```bash
>loader

Please Commandline flag
initAllFromDb : initDb + initObject + initData
initDb        : create user and db
dropDb        : delete user and db
initAll       : initObject + initData
initObject    : create table and other object on db
dropObject    : delete table and other object on db
initData      : create initial data
dropData      : delete all data
```

各コマンドはコメントの通りであまり重要ではないので割愛します。
実行についてはloader initAll とか叩けばDB側にアプリに必要なテーブルやらデータやらができるイメージです。

ローダーの起動方法について、初めはGoのテスト機能を簡単に使えばいいのでは？と思っていて、あまりこんなものを作る気はなかったのですが、テスト機能は当然テストのためのものなので、このパッケージだけテストを特殊な使い方をするのは後々まずいかなということでコマンドラインから実行するターゲットを指定する形をとりました。

Goでは標準パッケージにflagパッケージというコマンドラインの引数パーサをもっていて、これを利用しています。
flagパッケージは[ほかの方のまとめ](https://qiita.com/Yaruki00/items/7edc04720a24e71abfa2)や[公式Doc](https://golang.org/pkg/flag/)に使い方がまとまっています。
ローダーにおいては超シンプルでflag.Argで第一引数を抜いてSwitch文にかけてるだけです。
後々はちゃんとflag定義をしてDBの接続情報あたりをコマンドラインから入力する形にしてもいいかもですね。

```loader.go
import (
	"flag"
	"fmt"
)

func main() {
	flag.Parse()
	switch arg := flag.Arg(0); arg {
	case "initDb":
		fmt.Println("create user and db")
		if err := InitDb(); err != nil {
			fmt.Println(err)
		}
```

# ローダーのSQL実行部分

[前回の投稿](https://qiita.com/ponta-dev/items/a81e105c0aa047187e70)でSQLファイルを実行する方法についてまとめました。
それをもとに下記のようなユーティリティ関数を作っています。

```dbConnUtil.go
// ExecSQLFiles DB接続と実行するSQLファイルとそのリプレーサーを受け取り、ExecによるSQL実行を行う
func ExecSQLFiles(db *sql.DB, files []string, replacer *strings.Replacer) error {
	ctx := context.Background()
	ctx, cancel := context.WithTimeout(ctx, ConstTimeout)
	defer cancel()

	// Pingによる接続確認
	if err := db.PingContext(ctx); err != nil {
		return errors.New("Failed Ping Connection \n" + err.Error())
	}

	// 実行するSQLファイル分forを回す
	for index, path := range files {
		fmt.Printf("%d st called : %s try \n", index+1, path)
		// SQLファイルの読み込み
		str, err := ReadFileString(path)
		if err != nil {
			return errors.New("Failed Read Files \n" + err.Error())
		}

		// $1等の変数を自前でリプレースするリプレーサーの設定
		// SELECTとかでしか$1とかのプレースホルダが使えないため
		rstr := replacer.Replace(str)

		// SQLの実行
		_, err = db.ExecContext(ctx, rstr)
		if err != nil {
			return errors.New("Failed Execute SQL \n" + rstr + "\n" + err.Error())
		}
		fmt.Printf("%d st called : %s success \n", index+1, path)
	}

	return nil
}
```

本関数はローダーが接続するDBと、実行するSQLファイルのパスと、SQLファイルのリプレーサーを受け取ってSQLを順に実行する関数です。
前回の記事では触れなかったcontextによるタイムアウト設定や、PingによるDBの接続確認あたりも組み込んでいます。
リプレーサーをわざわざ引数に用意したのは、postgresの＄１や?がCREATE文では使えなかったことと、＄１とかより{user}とかの置換文字の方が後々よいかなと思ったからであまり深い意味はありません。
Goのリプレーサーはstringsパッケージの構造体で、下記のように奇数番目の引数を偶数番目の引数の文字列でリプレースしてくれるのでとても便利です。

```go
strings.NewReplacer("{user}", ConstUser, "{db}", ConstDBName)
```

あとはこのSQL実行関数を、dao～～.goファイルが呼び出すことで、必要なSQLファイル群を実行しています。
dao～～.goは接続先のDBと必要なリプレーサーと実行するSQLファイルのパス文字列の配列を管理する役割を持たせていますが、内容は特に特筆することはありません。

# 実行してみる

実際にローダーを実行するため空のDBを用意します。
![WS000004.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445416/5eecc03a-5b72-c718-2c18-dc927f6970c1.jpeg)

ローダーの起動（テーブル等のオブジェクト作成＋初期データ生成）

```bash
>loader initAll

initObject + initData
1 st called : loader/sql/table/CREATE_USERS.sql try
1 st called : loader/sql/table/CREATE_USERS.sql success
2 st called : loader/sql/table/CREATE_MEMO.sql try
2 st called : loader/sql/table/CREATE_MEMO.sql success
3 st called : loader/sql/seq/CREATE_USERS_ID_SEQ.sql try
3 st called : loader/sql/seq/CREATE_USERS_ID_SEQ.sql success
1 st called : loader/sql/dml/INSERT_USERS.sql try
1 st called : loader/sql/dml/INSERT_USERS.sql success
2 st called : loader/sql/dml/INSERT_MEMO.sql try
2 st called : loader/sql/dml/INSERT_MEMO.sql success
```

実行が成功したのでDBを確認すると・・・。

![WS000005.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445416/597bbb48-0764-1862-2cbf-2773e7b97f65.jpeg)

ちゃんとテーブルや初期データが入っていることが確認できます。
今度は削除してみます。

```bash
>loader dropObject

delete table and other object on db
1 st called : loader/sql/seq/DROP_USERS_ID_SEQ.sql try
1 st called : loader/sql/seq/DROP_USERS_ID_SEQ.sql success
2 st called : loader/sql/table/DROP_MEMO.sql try
2 st called : loader/sql/table/DROP_MEMO.sql success
3 st called : loader/sql/table/DROP_USERS.sql try
3 st called : loader/sql/table/DROP_USERS.sql success
```

実行が成功したのでまたDBを見てみます。
![WS000006.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445416/9800e036-900c-438b-0eb1-66ed1d9d618d.jpeg)

テーブルがすべて消えていますね。
他にも紹介はしませんがデータだけ消したり、データベースやユーザごと作ったりとか出来るようにしています。

# まとめ

DBの初期化ローダーを作ってみました。
基本的にGoの標準パッケージの枠組みですべて実装できるので、結構よさそうだと思っています。
「いやお前その命名はださいでしょ」とか「他にもこういう実装あるよ」とか突っ込みあれば頂けるとありがたいです。