Goの新定番？ORMのSQLBoilerを使ってみる

## はじめに

GoのORMといえば[Gorm](https://github.com/jinzhu/gorm)が定番ですが、
SQLBoilerというORMを使ってみたので、そのレポートです。
https://github.com/volatiletech/sqlboiler

* 既に存在するDBで動作する。DBスキーマを定義するツールではない。
* ActiveRecordのような高い生産性を実現する。
* Go本来のStructの使い勝手を壊さない。
* パフォーマンスが良い。

といった特徴があるようです。
パフォーマンスについてはREADME末尾にベンチマーク結果が載せてあり、他のORMと比較しても非常に優秀です。
https://github.com/volatiletech/sqlboiler#benchmarks

この記事に掲載しているコードは、以下のリポジトリにあります。
https://github.com/uhey22e/sqlboiler-tutor

## 環境

* Go v1.11
* SQLBoiler v3.0.1
* PostgreSQL v10.5

## DBの準備

簡単なToDoアプリを模して、以下のようなスキーマを用意します。
DBはPostgreSQL 10.5で動作を確認しています。

```sql:sql/10_todo.sql
CREATE TABLE todo (
    id bigserial NOT NULL,
    title varchar(255) NOT NULL,
    note text,
    finished boolean NOT NULL,
    due_date timestamp,
    CONSTRAINT todo_pkc PRIMARY KEY(id)
);
```

## コードの生成

SQLBoilerは、DBからテーブル構造を読み取り、各テーブルに対応するStructを生成します。
また、Structの自動生成と同時に、InsertやSelectなど各DB操作を行うメソッドも自動生成されます。
このような特徴から、SQLBoilerはリフレクションを用いる他のORMと違い、型安全で高速だそうです。

DBからコードを自動生成するために、まずは設定ファイル `sqlboiler.toml`をプロジェクトルートに作成します。

```
$ cat sqlboiler.toml
[psql]
  dbname = "sample_db"
  host   = "localhost"
  port   = 5432
  user   = "postgres"
  pass   = "postgres"
  sslmode= "disable"
  blacklist = []
```

`dbname`、`host`などは接続先DBの情報です。
`blacklist`は、モデルを自動生成"しない"テーブルを指定することが出来ます。マイグレーション用のテーブルがある場合などに便利そうです。
また、`whitelist`というオプションを使うと、モデルを自動生成するテーブルを選択することが出来ます。`blacklist`とどちらか一方の指定となります。

設定ファイルが出来たら、コード生成です。
まずは必要なパッケージの入手から。
SQLBoilerと、PostgreSQL用のドライバを`go get`します。

```shell
$ go get -u -t github.com/volatiletech/sqlboiler
$ go get github.com/volatiletech/sqlboiler/drivers/sqlboiler-psql
```

自動生成は`sqlboiler`コマンドで実行出来ますが、コマンドを忘れないように`go generate`を使いましょう。

```go:main.go
package main
//go:generate sqlboiler --wipe psql
```

コード生成が成功すれば、以下のようなディレクトリとファイルが生成されているはずです。
`todo.go`に、テーブルと対応関係のあるStructやメソッドが実装されている状態です。

```
$ tree ./models
./models
├── boil_main_test.go
├── boil_queries.go
├── boil_queries_test.go
├── boil_suites_test.go
├── boil_table_names.go
├── boil_types.go
├── psql_main_test.go
├── psql_suites_test.go
├── psql_upsert.go
├── todo.go     # このファイルがテーブル"todo"に対応している
└── todo_test.go
```

生成されたStructは、以下の通りです。
カラム名はsnake_caseでしたが、フィールド名ではUpperCamelCaseに変換してくれています。
また、json用タグなども一緒に生成されていて便利そうです。
これらタグのcaseは、自動生成時のオプションでsnake_caseかcamelCaseを選択可能です。

```go:models/todo.go
// いろいろ省略
type Todo struct {
	ID       int64       `boil:"id" json:"id" toml:"id" yaml:"id"`
	Title    string      `boil:"title" json:"title" toml:"title" yaml:"title"`
	Note     null.String `boil:"note" json:"note,omitempty" toml:"note" yaml:"note,omitempty"`
	Finished bool        `boil:"finished" json:"finished" toml:"finished" yaml:"finished"`
	DueDate  null.Time   `boil:"due_date" json:"due_date,omitempty" toml:"due_date" yaml:"due_date,omitempty"`

	R *todoR `boil:"-" json:"-" toml:"-" yaml:"-"`
	L todoL  `boil:"-" json:"-" toml:"-" yaml:"-"`
}
```

## DB接続

SQLBoilerではDB接続に関する機能は用意されていませんので、
`database/sql`を使用し、DBコネクションを作ります。

今回はgoのテストの仕組みを使ってコードを動かします。
`App`というStructは、各機能をメソッドとして実装するためのものです。

```go:todo/todo_test.go
func TestMain(m *testing.M) {
	// connect to db
	psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
		"localhost", 5432, "postgres", "postgres", "sample_db",
	)
	db, err := sql.Open("postgres", psqlInfo)
	if err != nil {
		panic(err)
	}
	defer db.Close()

	// validate whether or not the connection string was correct
	if err := db.Ping(); err != nil {
		panic(err)
	}

	app := &App{
		db: db,
	}
}
```

## 基本的なDB操作

### Insert

Sliceを引数に取り、1件以上のTodoをDBに保存するメソッドを実装しました。

```go:todo/todo.go
// Store stores a one or more todos into DB
func (a *App) Store(obj []*models.Todo) error {
	// insert for each todo
	for _, v := range obj {
		if err := v.Insert(context.Background(), a.db, boil.Infer()); err != nil {
			return err
		}
	}
	return nil
}
```

forループでInsert文を回しているのは、SQLBoilerにBulk insert機能が無いためです。
https://github.com/volatiletech/sqlboiler/issues/364
大量のデータをまとめてInsertする用途では、生SQLを書いたほうがいいかもしれません…

このメソッドを、以下のテストコードで試してみます。
`null.TimeFrom`などの関数は、Nullableなカラムの値を取り扱うための`null.Time`などの型に関連するものです。
https://github.com/volatiletech/null

```go:todo/todo_test.go
func TestStore(t *testing.T) {
	// prepare sample insert data
	now := time.Now().UTC()
	todos := []*models.Todo{
		&models.Todo{
			Title:    "Sample ToDo 1",
			DueDate:  null.TimeFrom(now.Add(3 * time.Hour)),
			Note:     null.StringFrom("note1..."),
			Finished: false,
		},
		&models.Todo{
			Title:    "Sample ToDo 2",
			DueDate:  null.TimeFrom(now.Add(6 * time.Hour)),
			Note:     null.StringFrom("note2..."),
			Finished: false,
		},
	}

	// store
	if err := testApp.Store(todos); err != nil {
		t.Fatal(err)
	}

	for _, v := range todos {
		t.Log(fmt.Sprintf("%+v", v))
	}
}
```

無事にInsertできました。

```
sample_db=# select * from todo;
 id |     title     |   note   | finished |          due_date
----+---------------+----------+----------+----------------------------
  1 | Sample ToDo 1 | note1... | f        | 2018-09-15 14:34:58.605844
  2 | Sample ToDo 2 | note2... | f        | 2018-09-15 17:34:58.605844
(2 rows)
```

### Select

`finished == false`なレコードをSelectするメソッドを実装してみます。
Where句をオプションで指定するのですが、
`github.com/volatiletech/sqlboiler/queries/qm`を別途Importする必要があります。

`All`メソッドはヒットするレコードが無いとエラーを返すのですが、
ヒットしない場合には空のスライスを返したいので、一度Countを実行しています。

```go:todo/todo.go
// FetchUnfinished fetches unfinished ToDos
func (a *App) FetchUnfinished() ([]*models.Todo, error) {
        queries := []qm.QueryMod{
                qm.Where(models.TodoColumns.Finished+"=?", false),
        }

        // count up records
        counts, err := models.Todos(queries...).Count(context.Background(), a.db)
        if err != nil {
                return nil, err
        } else if counts == 0 {
                return make([]*models.Todo, 0), nil
        }

        // fetch!
        return models.Todos(queries...).All(context.Background(), a.db)
}
```

以下のテストコードで無事にSelectできました。
Structの自動生成時にjsonタグも付いてくるので、jsonエンコードもよしなにやってもらえます。

```go:todo/todo_test.go
func TestFetchUnfinished(t *testing.T) {
    todos, err := testApp.FetchUnfinished()
    if err != nil {
        t.Fatal(err)
    }
    for _, v := range todos {
        var buf bytes.Buffer
        if err := json.NewEncoder(&buf).Encode(v); err != nil {
            t.Fatal(err)
        }
        t.Log(buf.String())
    }
}
```

```
$ go test -v ./todo --run TestFetchUnfinished
=== RUN   TestFetchUnfinished
--- PASS: TestFetchUnfinished (0.00s)
    todo_test.go:80: {"id":1,"title":"Sample ToDo 1","note":"note1...","finished":false,"dueDate":"2018-09-15T14:34:58.605844Z"}

    todo_test.go:80: {"id":2,"title":"Sample ToDo 2","note":"note2...","finished":false,"dueDate":"2018-09-15T17:34:58.605844Z"}

PASS
ok      github.com/uhey22e/sqlboiler-tutor/todo 0.010s
```

### Update

1つ以上の指定したIDのToDoを、`finished = true`にUpdateするメソッドを実装しました。
WhereInでIDを指定するのですが、このメソッドは引数が`[]interface{}`型のため、キャストが必要になります。
https://github.com/volatiletech/sqlboiler/issues/227
Updateするカラムと値の指定は、`map[string]interface{}`型の変数で行います。
Keyがカラム名、ValueがUpdate後の値です。

```go:todo/todo.go
// Finish update one or more records selected by ID
func (a *App) Finish(ids []int64) error {
    // Set updating columns
    updCols := map[string]interface{}{
        models.TodoColumns.Finished: true,
    }

    // WhereIn method needs to pass a slice of interface{}
    targetIDs := make([]interface{}, len(ids))
    for i, v := range ids {
        targetIDs[i] = v
    }
    query := qm.WhereIn(models.TodoColumns.ID+" IN ?", targetIDs...)

    // update!
    _, err := models.Todos(query).UpdateAll(context.Background(), a.db, updCols)

    return err
}
```

以下のテストコードで無事にUpdateできました。

```go:todo/todo_test.go
func TestFinish(t *testing.T) {
    if err := testApp.Finish([]int64{1, 2}); err != nil {
        t.Fatal(err)
    }
}
```
```
sample_db=# select * from todo;
 id |     title     |   note   | finished |          due_date
----+---------------+----------+----------+----------------------------
  1 | Sample ToDo 1 | note1... | t        | 2018-09-15 14:34:58.605844
  2 | Sample ToDo 2 | note2... | t        | 2018-09-15 17:34:58.605844
(2 rows)
```

## コード生成のカスタマイズ

SQLBoilerのコード生成は、オプションによってカスタマイズすることが出来ます。
ここからがSQLBoilerの真髄…？
https://github.com/volatiletech/sqlboiler#controlling-generation

### 型の変更

PostgreSQLにて`timestamp`型のカラムがあった場合、SQLBoilerはデフォルトでは`time.Time`あるいは`null.Time`型を使用しますが、
この型をカスタマイズすることが可能です。
https://github.com/volatiletech/sqlboiler#types

例えば、`time.Time`型をJSONエンコードする際は、UnixTimestampにしたいというシチュエーションを考えてみます。
JSONエンコードの挙動を変更するには、対象の型をembedした新たなStructを定義し、`MarshalJSON`メソッドを再実装することで、`json.Marshaler`インタフェースを実装する、というのがひとつのアプローチです。

このアプローチにて、`null.Time`型をembedした`NullTime`というStructを新たに定義し、SQLBoilerのコードでもこの型を使うようにしてみます。

```go:types/time.go
package types

import (
    "encoding/json"
    "time"

    "github.com/volatiletech/null"
)

type NullTime struct {
    null.Time
}

func (t NullTime) MarshalJSON() ([]byte, error) {
    var ts int64
    if !t.Time.Valid {
        return json.Marshal(nil)
    }
    ts = t.Time.Time.Unix()
    return json.Marshal(ts)
}

func NullTimeFrom(t time.Time) NullTime {
    return NullTime{
        Time: null.TimeFrom(t),
    }
}
```

型のカスタマイズは以下のように、SQLBoilerの設定ファイルに追記します。

```toml:sqlboiler.toml
[[types]]
  [types.match]
    type = "null.Time"
    nullable = true

  [types.replace]
    type = "mytypes.NullTime"

  [types.imports]
    third_party = ['mytypes "github.com/uhey22e/sqlboiler-tutor/types"']
```

これでコードの再生成を行うと、モデルは以下のようになります。

```go:models/todo.go
// Todo is an object representing the database table.
type Todo struct {
    ID       int64            `boil:"id" json:"id" toml:"id" yaml:"id"`
    Title    string           `boil:"title" json:"title" toml:"title" yaml:"title"`
    Note     null.String      `boil:"note" json:"note,omitempty" toml:"note" yaml:"note,omitempty"`
    Finished bool             `boil:"finished" json:"finished" toml:"finished" yaml:"finished"`
    DueDate  mytypes.NullTime `boil:"due_date" json:"dueDate,omitempty" toml:"dueDate" yaml:"dueDate,omitempty"`
    R        *todoR           `boil:"-" json:"-" toml:"-" yaml:"-"`
    L        todoL            `boil:"-" json:"-" toml:"-" yaml:"-"`
}
```

この状態で先程の`FetchUnfinished`メソッドを実行すると、以下のような結果が得られます。メソッドの修正は必要ありません。
`dueDate`がきちんとUnixTimestampになっています。データが少し変わっているのは、null値のエンコードを見るためにデータを入れ直したためです。
多少であれば手書きでも良いかもしれませんが、テーブルが増えてきたときのことを考えると、この仕組みはありがたいです。

```
$ go test -v ./todo --run TestFetchUnfinished
=== RUN   TestFetchUnfinished
--- PASS: TestFetchUnfinished (0.00s)
    todo_test.go:86: {"id":1,"title":"Sample ToDo 1","note":"note1...","finished":false,"dueDate":1537086524}

    todo_test.go:86: {"id":2,"title":"Sample ToDo 2","note":"note2...","finished":false,"dueDate":1537097324}

    todo_test.go:86: {"id":3,"title":"Sample ToDo 3","note":"note2...","finished":false,"dueDate":null}

PASS
ok      github.com/uhey22e/sqlboiler-tutor/todo 0.009s
```

### テーブル名のエイリアスの変更

SQLBoilerは、snake_caseのテーブル名・カラム名に対して、良い感じにUpperCamelCase/LowerCamelCaseの名前を生成してくれますが、これを変更することが出来ます。
https://github.com/volatiletech/sqlboiler#aliases

今回は`todo`というテーブル名に対して`Todo`というStructを生成してくれていますが、これを`ToDo`に、
`finished`というカラム名に対して`Finished`というフィールド名を生成してくれていますが、これを`IsFinished`に変更してみます。

```sqlboiler.toml
[aliases.tables.todo]
  up_plural = "ToDos"
  up_singular = "ToDo"
  down_plural = "todos"
  down_singular = "todo"

  [aliases.tables.todo.columns]
    finished = "IsFinished"
```

上記を設定ファイルに追記すると、Structは以下のようになります。

```go:models/todo.go
type ToDo struct {
    ID         int64            `boil:"id" json:"id" toml:"id" yaml:"id"`
    Title      string           `boil:"title" json:"title" toml:"title" yaml:"title"`
    Note       null.String      `boil:"note" json:"note,omitempty" toml:"note" yaml:"note,omitempty"`
    IsFinished bool             `boil:"finished" json:"finished" toml:"finished" yaml:"finished"`
    DueDate    mytypes.NullTime `boil:"due_date" json:"dueDate,omitempty" toml:"dueDate" yaml:"dueDate,omitempty"`
    R          *todoR           `boil:"-" json:"-" toml:"-" yaml:"-"`
    L          todoL            `boil:"-" json:"-" toml:"-" yaml:"-"`
}
```

正直あまり使い所が思いつきませんが、
DBのカラム名が変更になった際、Goのコードを修正するのが大変なときに、一時しのぎとして使えるかもしれません。
あるいは、DBのテーブル名に諸々の都合でPrefix/Suffixをつけているが、Goのコードではこれを排除したい、という場合にも使えるかもしれません。

### テンプレートの追加

自動生成のカスタマイズではカバーしきれない場合、自前でテンプレートを書くことで自動生成されるコードを追加することが出来ます。
https://github.com/volatiletech/sqlboiler#templates

先程のInsertのメソッドではfor文を書きましたが、
毎回似たようなコードを書くのも大変なので、これを自動生成してみます。
モデルのSliceに対してメソッドを追加する形でテンプレートを書いてみました。

```go:boil_templates/90)multi_insert.go.tpl
{{- $alias := .Aliases.Table .Table.Name}}

// Insert inserts multiple records
func (o {{$alias.UpSingular}}Slice) Insert({{if .NoContext}}exec boil.Executor{{else}}ctx context.Context, exec boil.ContextExecutor{{end}}, columns boil.Columns) error {
    for _, v := range o {
        if err := o.Insert({{if not .NoContext}}ctx, {{end -}} exec, columns); err != nil {
            return err
        }
    }
    return nil
}
```

テンプレートを追加した場合には、それを保存したディレクトリを生成時に指定する必要があります。
標準のテンプレートも指定する必要があるのでご注意を。

```
sqlboiler --wipe \
--templates ${GOPATH}/src/github.com/volatiletech/sqlboiler/templates,\
${GOPATH}/src/github.com/volatiletech/sqlboiler/templates_test,\
${GOPATH}/src/github.com/uhey22e/sqlboiler-tutor/boil_templates \
--struct-tag-casing camel psql
```

生成されたコードは以下です。

```go:models/todo.go
// Insert inserts multiple records
func (o ToDoSlice) Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error {
    for _, v := range o {
        if err := o.Insert(ctx, exec, columns); err != nil {
            return err
        }
    }
    return nil
}
```

ただfor文を回すだけです。Insert文は減りません。
今回はテーブルをひとつしか使っていませんが、すべてのテーブルに対して同様のコードが生成されます。

## おわりに

他言語のORMを触った経験が少ないため、比較はあまり出来ませんが、
リフレクションを使った複雑なコードになっていないため、
DB周りで何かおかしなことがあったときに原因が追いやすい、というのはあると思います。

今回はひとつのテーブルに対しての操作しか試していませんが、
Eager Loadingも実装されているようなので、リレーションについても追って試してみたいと思います。