♨️

GoにおけるORMと、SQLBoiler入門マニュアル

2021/07/31に公開

前置き

この文章は技術書典9で頒布した「INSIDE MERY3」に寄稿した文章をベースに、一部修正を加えたものです。「INSIDE MERY3」が頒布終了したため、改めて(記事としては、少し長いですが)公開します。

はじめにGoにおける代表的なORMライブラリをいくつか紹介し、続いてその中からSQLBoilerの使い方を解説します。ORMに詳しくない読者も対象にしているため、詳しい方には不要な情報もあるかもしれません。その場合は適宜読み飛ばしていただければと思います。

Goで使われる代表的なORMライブラリ

ORMとはどういう技術か

ORMとはObject-relational mappingの略語です。一般的にリレーショナルデータベース内のレコードをあつかう際には、データベースのための専用言語であるSQLを使います。しかしデータベースはデータを効率的に管理/検索することに特化した構造となっているため、アプリケーションを実装する際によく使われるオブジェクト指向の構造とは大きな差異があります。

ORMはこのリレーショナルデータベースとオブジェクト指向のふたつの世界の構造を、自動でマッピングしてくれる技術です。具体的には次のふたつの機能から構成されます。

  • オブジェクト(Goでは構造体)とデータベースから取得したレコードを関連づける
  • SQL文を組み立てる

Goにもいくつかの代表的なORMライブラリがありますが、標準ライブラリのみを使って実装することも好まれがちです。ORMライブラリを使わない場合は、標準ライブラリであるdatabase/sqlのScan関数を用いてORMでおこなうような関連付けを行います。

database/sqlのScan関数を使ってモデルにデータを読み込むには、次のように実装します。

database/sqlでの実装例(参照)
import (
	"database/sql"
)

type Article struct {
	ID    int64
	Title string

	db *sql.DB
}

func NewArticle(db *sql.DB) *Article {
	return &Article{
		db: db,
	}
}

func (a *Article) Load(articleID int64) error {
	row := a.db.QueryRow("SELECT * FROM articles WHERE id = ?", articleID)

	var id int64
	var title string
	err := row.Scan(&id, &title)
	if err != nil {
		return err
	}

	a.ID = id
	a.Title = title

	return nil
}

構造体に直接読み込むことはできないので、読み込み部分を別途実装する必要があります。

更新系のクエリの場合は次のようにプリペアドステートメントを生成して、実行します。プリペアドステートメントに引数として渡した値は自動でサニタイズされます。

database/sqlでの実装例(更新)
func (a *Article) Create() error {
	p, err := a.db.Prepare("INSERT INTO articles (title) VALUES (?)")
	if err != nil {
		return err
	}

	r, err := p.Exec(a.Title)
	if err != nil {
		return err
	}

	a.ID, err = r.LastInsertId()
	if err != nil {
		return err
	}

	return nil
}

データベースとやり取りする部分はWEBアプリケーションでは頻繁に登場するにもかかわらず、似たようなコードを都度実装することになりがちです。ORMライブラリを使うことで、そのようないわゆるボイラープレートコードを減らし、本質的な実装に注力することができます。

GoのORMライブラリとして有名なものはSQLBoiler以外にも多くあり、乱立状態にあるといえます。比較のために代表的なものをいくつか紹介します。

GORM

GORMは開発者に扱いやすく、かつ機能が充実していることをセールスポイントにしているORMライブラリです。

実際にGORMは他のORMライブラリと比較して、もっとも機能が多いもののひとつであり、マイグレーションなども可能です。マイグレーションとは、運用中のデータベースにデータを入れたまま、テーブルを追加したりカラムを変更するなどして、スキーマを管理する機能です。

参照系の実装は次のようになります。

GORMでの実装例(参照)
import (
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/sqlite"
)

type Article struct {
	gorm.Model
	db    *gorm.DB
	ID    int64 `gorm:"primary_key`
	Title string
}

func NewArticle(db *gorm.DB) *Article {
	return &Article{
		db: db,
	}
}

func (a *Article) Load(articleID int64) error {
	a.db.First(a, articleID)

	return nil
}

続いて更新系の記述方法を紹介します。

GORMでの実装例(更新)
func (a *Article) Create() error {
	if a.db.NewRecord(a) {
		res := a.db.Create(a)
		if res.Error != nil {
			return res.Error
		}

		res.Scan(a)
	}
	return nil
}

参照系、更新系ともに、Scan関数で実装した場合と比べ、非常に少ない行数で実装することができています。

GORMは内部的にリフレクションを使ってORMを実現しています。ブラックボックスとなる部分が多いため実際に生成されるSQLが見えにくく、またフィールドに渡す型が誤っていてもコンパイルが通ってしまうところがデメリットといえます。

GORMのv1では主キーがゼロ値の構造体を渡すと更新/削除時のWhere句が生成されないので、引数などを適切に検査しないとテーブルの全データが削除される不具合が入り込んだりもします(最近リリースされたv2ではこの問題はありません)。この辺りは、ブラックボックスな部分の悪い例であるともいえます。

sqlx

sqlxdatabase/sqlを拡張したサードパーティのライブラリです。

sqlxにはSQL文を組み立てる機能はないため、厳密にはORMとはいえないかもしれませんが、ORMの代替として非常によく使われており、比較のために紹介します。

sqlxがdatabase/sqlと大きく違う点は、Scan関数で構造体を使用することができることです。

sqlxでの実装例(参照)
import (
	"github.com/jmoiron/sqlx"
)

type Article struct {
	ID    int64  `db:"id"`
	Title string `db:"title"`

	db *sqlx.DB
}

func NewArticle(db *sqlx.DB) *Article {
	return &Article{
		db: db,
	}
}

func (a *Article) Load(articleID int64) error {
	row := a.db.QueryRow("SELECT * FROM articles WHERE id = ?", articleID)

	err := row.Scan(a)
	if err != nil {
		return err
	}

	return nil
}

database/sqlではプリペアドステートメントを作成してから、Exec関数でクエリを実行していました。一方sqlxではNamedExec関数を使うことで、見通しのよい更新処理を実装することができます。

sqlxでの実装例(更新)
func (a *Article) Create() error {
	r, err := a.db.NamedExec("INSERT INTO articles (title) VALUES (:title)",
		map[string]interface{}{
			"title": a.Title,
		},
	)

	a.ID, err = r.LastInsertId()
	if err != nil {
		return err
	}

	return nil
}

database/sqlと比較すると、とてもすっきりとシンプルに実装できています。

ちなみに、sqlxでも構造体のマッピングはリフレクションを使っておこなっています。

SQLBoiler

最後にSQLBoilerの例も記述します。SQLBoilerが他のORMライブラリと異なる点は、データベース内のテーブルからコードが自動生成されるところです。

コードの生成にはコマンドラインツールを利用します。

sqlboiler mysql -c config.toml -o models --no-tests

この例では、sample/modelsというパッケージ名でコード生成されたものを、
自前の構造体に埋め込んで使用します。

SQLBoilerでの実装例(構造体埋め込み)
import (
	"context"
	"database/sql"

	"github.com/volatiletech/sqlboiler/v4/boil"

	"sample/models"
)

type Article struct {
	*models.Article

	db *sql.DB
}

参照の例です。

SQLBoilerでの実装例(参照)
func NewArticle(db *sql.DB) *Article {
	return &Article{
		db: db,
	}
}

func (a *Article) Load(ctx context.Context, id int64) error {
	m, err := models.FindArticle(ctx, a.db, id)
	if err != nil {
		return err
	}

	a.Article = m

	return nil
}

更新処理です。

SQLBoilerでの実装例(更新)
func (a *Article) Create(ctx context.Context) error {
	err := a.Insert(ctx, a.db, boil.Infer())
	if err != nil {
		return err
	}

	return nil
}

SQLBoilerもGORMと同じくらい少ない記述で実装することができます。

SQLBoilerのよいところ

筆者はORMライブラリとして、SQLBoilerを選定することが多いです。

SQLBoilerはコードを事前に生成するタイプのORMライブラリです。そのため、SQLBoilerは静的に型付けされており、実行時にリフレクションを使う必要がないため、高速に動作します。また、生成されたコードを直接読むことで、挙動を把握しやすいため、ブラックボックスとなる部分が少ないこともメリットです。

リフレクションを使ったORMライブラリでは構造体が実行時に生成されるので、フィールドに渡す型に誤りがあった場合、コンパイル時に気づくことができません。一方SQLBoilerはコンパイル時にエラーが出るので、すぐに誤りを修正することでき、安全です。

GORMなどと比べて機能的に不足している面はあるかもしれませんが、例えばマイグレーションはORMライブラリで行わなくても、マイグレーション専用のライブラリも多くあり、それぞれ組み合わせて使う方がシンプルであるともいえます。

ORMライブラリの特殊な機能を使う場合はチームメンバーがそのツールについて学習する必要があります。そのようなイレギュラーなケースの場合、あえてSQL文やロジックを自前で組み立てた方が、むしろ明示的でわかりやすいのではないかと考えています。

SQLBoilerの使い方

それでは、実際にSQLBoilerの使い方を解説していきたいと思います。

データベースファースト

SQLBoilerはデータベースファーストのORMライブラリであり、実装の前にデータベースにあらかじめテーブルが存在している必要があります。そのため、はじめにデータベースのスキーマを定義してから、開発用のデータベースにスキーマを流し込み、その後定義された各テーブルを操作するモデルをコマンドで生成していきます。

コマンドの実行準備として、まずはSQLBoilerの設定ファイルを書きます。モデルを生成するために、データベースにアクセスするための接続情報と、モデルとして書き出すテーブルを明示的に指定します。

設定ファイルはTOML、YAMLもしくはJSONで記述できます。

TOMLで設定を書く場合、次のようになります。

[mysql]
  dbname     = "db"
  host       = "127.0.0.1"
  port       = 3306
  user       = "user"
  pass       = "pass"
  sslmode    = "false"
  whitelist = [
    "articles",
    "users"
  ]

設定ファイル内のデータベースの接続情報はコード生成のためのコマンドラインツールのみが使用するためのものです。そのため一般にはローカル開発用のデータベースを参照することになると思います。アプリケーション実行時にはdatabase/sqlパッケージのOpen関数から取得したsql.DB構造体を直接渡すので、ここでの設定情報は使われません。

whitelistにはコード生成の対象にするテーブルを明示的に指定します。blacklistで特定のテーブルを生成させないこともできます。

blacklist = [
  "migrations",
  "too_old_and_unused_table"
]

whitelistとblacklistどちらにも同じテーブルを入れた場合、whitelistが優先されます。不要なコード生成をおこないたくない場合は、whitelistで明示的に指定することが望ましいです。生成対象にするテーブルは必ずプライマリキーを持つ必要があります。

続いて必要なライブラリを取得します。

go install github.com/volatiletech/sqlboiler/v4@v4.6.0
go install github.com/volatiletech/sqlboiler/v4/drivers/sqlboiler-mysql@v4.6.0

アプリケーションに依存関係を組み込む際にはバージョンの指定を必ずおこなわれるようにします。またnullモジュールも必要になるかもしれません。

go get github.com/volatiletech/sqlboiler/v4
go get github.com/volatiletech/null/v8

続いてコードを生成するコマンドを実行します。先ほど例であげたコマンドにいくつかオプションを追加しました。

sqlboiler mysql -c config/database.toml -o models -p models --no-tests --wipe

今回はconfig/database.tomlに置いた設定ファイルを読み取って、modelsディレクトリにコードを生成しています。生成時のパッケージ名はmodelsとしました。

--no-testsオプションをつけることで、テストコードについては自動生成しないようにしています。また--wipeオプションによって、再生成する際に前のコードを削除するようにしています。

コード生成は頻繁におこなうため、Makefileに入れておくことをお勧めします。

DB接続

前述のとおり、SQLBoilerではOpen関数を使って、データベースに接続します。

import (
	"database/sql"

	_ "github.com/go-sql-driver/mysql"
)

...

db = sql.Open("mysql", "root":@localhost:3306/sqlboiler_sample")

m, err := models.FindArticle(ctx, db, 1)

SQLBoilerの基本的な使い方

Select

SQLBoilerではqmパッケージを使ってクエリを組み立てていきます。

クエリの組み立て
import (
  "github.com/volatiletech/sqlboiler/v4/queries/qm"
)

...

m, err := models.Articles(
	qm.Where("title=?", "title1"),
	qm.And("author_id=?", 1),
	qm.Or("id=?", 1),
	qm.Limit(5),
	qm.Offset(6),
).One(ctx, a.db)

Where句の場合、コード生成された関数を使ってクエリを組み立てることもできます。型の安全性の点からメリットが大きいので、積極的に使っていきたいです。

Where関数
m, err := models.Articles(
	models.ArticleWhere.AuthorID.EQ(1), // AuthorIDが1と一致
	models.ArticleWhere.ID.GTE(10000), // IDが10000よりも大きい
	models.ArticleWhere.Title.IsNotNull(), // TitleがNullでない
).One(ctx, a.db)

組み立てたクエリはフィニッシャー関数で実行されます。フィニッシャーの一つであるOne関数は条件に合致する構造体を1件のみ取得します("LIMIT 1"をSQL文に付与します)。複数件を取得したい場合はAll関数を使用します。返り値は構造体のスライスです。

All関数
ms, err := models.Articles(
	models.ArticleWhere.AuthorID.EQ(1),
).All(ctx, a.db)

プライマリキーで取得する場合は、構造体名を後置した、Find関数を利用することもできます。

Find関数
m, err := models.FindArticle(ctx, a.db, 1)

SELECTクエリを実行してデータが存在しない場合は、エラーとしてsql.ErrNoRowsが返ります。

Enums

テーブルで定義した列挙型は定数として生成されます。

生成されたEnum定数
const (
	ArticlesStatusDRAFT     = "DRAFT"
	ArticlesStatusSUBMITTED = "SUBMITTED"
	ArticlesStatusREVIEWED  = "REVIEWED"
	ArticlesStatusPUBLISHED = "PUBLISHED"
)

そのため、コード内でテーブル内のenum値を二重定義する必要はありません。テーブルからコード生成された定数を、そのままモデルにセットすることができます。

列挙型の値を代入する
m.Status = models.ArticlesStatusPUBLISHED

IN句

引数に配列を渡して、IN句をビルドすることもできます。

IN句
m, err := models.Articles(
	qm.WhereIn("status", []string{models.ArticlesStatusDRAFT, models.ArticlesStatusSUBMITTED}),
	qm.AndIn("author_id", Ints64ToInterfaces([]int64{1, 2, 3})),
).One(ctx, a.db)

注意点として、SQLBoilerのWhereInAndIn関数が取る引数はinterface{}型の配列です。したがって、数値型の場合はinterface{}型に変換してから渡す必要があります。ここでは変換のためにInts64ToInterfaces関数を追加しました。

Ints64ToInterfaces関数
func Ints64ToInterfaces(nums []int64) []interface{} {
	results := make([]interface{}, len(nums))

	for i, n := range nums {
		results[i] = n
	}

	return results
}

Count

コード生成された構造体では、Count関数も利用可能です。任意の条件に該当するレコードの数を調べることができます。

Count関数
count, err := models.Articles(
	models.ArticleWhere.Status.EQ(models.ArticlesStatusPUBLISHED),
).Count(ctx, a.db)

Exists

Exists関数もコード生成されます。任意の条件に当てはまるレコードが存在するかを調べるときに使います。

Exists関数
ok, err := models.Articles(
	models.ArticleWhere.Status.EQ(models.ArticlesStatusPUBLISHED),
).Exists(ctx, a.db)

Insert

レコードの新規作成はInsert関数でおこないます。

//go:Insert
article := &models.Article{
	Title:    null.StringFrom("title"),
	AuthorID: 1,
}

err := article.Insert(ctx, a.db, boil.Infer())

第2引数に、boil.Inferを指定すると、Goの構造体のゼロ値とテーブルのデフォルト値から、更新すべきフィールドを自動推論します。

INSERT文に含めるフィールドを明示したい場合は、boil.Whitelistを使用します。この場合指定しなかったフィールドは更新されません。

CreatedAtUpdatedAtは通常指定しなくても、自動的に更新されますが、boil.Whitelistを指定した場合は更新されません。

INSERT(Whitelist)
err := article.Insert(ctx, a.db, boil.Whitelist("Title", "AuthorID"))

boil.Blacklistを指定した場合は、基本的にboil.Inferと同じ挙動ですが、指定したフィールドは除外します。boil.Graylistを指定した場合、boil.Inferと同じ挙動に加えて、指定したフィールドを更新対象に加えます。

Update

すでに存在するレコードの更新もInsertと似ています。Update関数を実行します。

Update
m, err := models.FindArticle(ctx, a.db, 1)

if err != nil {
	return err
}

m.Title = "new title"

_, err := m.Update(ctx, a.db, boil.Infer())

Update関数の第1返り値は、変更された行数です。

第2引数にはInsertと同様のオプションを指定します。UpdatedAtは自動更新されますが、boil.Whitelistで更新するフィールドを明示した場合は更新されません。

INSERTのときはboil.Inferで基本的に問題ありませんが、UPDATEのときは変更する値がゼロ値の場合には注意が必要です。例えばフィールドの型が真偽値で値を偽にしたい場合は、boil.Inferだと変更されない場合があります。この場合、boil.Graylistで真偽値のフィールドを追加するなどの対応が必要になります。

複数行を一気に更新することもできます。Mは文字列をキーとしたmapです。

UpdateAll
ms, err := models.Articles(
	models.ArticleWhere.AuthorID.EQ(1),
).All(ctx, a.db)

if err != nil {
	return err
}

_, err = ms.UpdateAll(ctx, a.db, models.M{"status": models.ArticlesStatusDRAFT})

Upsert

Upsertは、Insertがテーブルのユニーク制約でエラーになった場合に、自動的にUpdateをおこなうようなSQL文を実行してくれる関数です。

Upsert
author := &models.Author{
	Name: "gami",
}

author.Upsert(ctx, a.db, boil.Infer(), boil.Infer())

第3引数はUpdate用、第4引数はInsert用のオプションです。Upsertの処理はデータベースエンジンによってSQL文の仕様が異なるため、使用するデータベースに応じて関数のインターフェースが異なります。今回はMySQLの例です。

Delete

Delete文を実行して、構造体を削除します。

Delete
m, err := models.FindArticle(ctx, a.db, 1)

if err != nil {
	return err
}

_, err = m.Delete(ctx, a.db)

最初の返り値は削除された行数です。対象の行のみを削除するので、Eager Loadingされた構造体を削除することはありません。

DeleteAll
ms, err := models.Articles(
	models.ArticleWhere.AuthorID.EQ(1),
).All(ctx, a.db)

if err != nil {
	return err
}

_, err = ms.DeleteAll(ctx, a.db)

DeleteAll関数で構造体のスライスを一括削除することもできます。

Reload

何らかの理由でデータベースの中身とアプリケーション内の構造体の同期が取れなくなった場合は、Reload関数を使って再読み込みすることができます。

Reload
m, err := models.FindArticle(ctx, a.db, 1)

if err != nil {
	return err
}

err = m.Reload(ctx, a.db)

ReloadAll関数で複数の構造体を一度に同期させることもできます。

Eager Loadingされた構造体は再読み込みされないため、必要であれば個別に読み込み直す必要があります。

SQLBoilerの応用的な使い方

Eager Loading

SQLBoilerでは、外部キー制約で関連づけられた1対1、もしくは1対多の構造に相当するモデルを、
構造体のフィールドに読みこませることができます。ORMでは、このような関連するモデルをまとめて取得することをEager Loadingと呼びます。外部キー制約がない場合はEager Loadingをおこなうことができず、また、多対多の関連は表現できません。

Eager Loadingを行いたい場合は、クエリを組み込む際にLoad関数を追加します。

Eager Loading
m, err := models.Articles(
	models.ArticleWhere.Title.EQ(null.StringFrom("title1")),
	qm.Load("Author"),
).One(ctx, a.db)

if err != nil {
	return err
}

fmt.Printf("author_name=%s", m.R.Author.Name)

Eager Loadingを使わずに次のような処理を実装した場合、取得したレコードの数に比例して処理が発生します。一般にこれをN+1問題と呼びます。N+1が存在する場合、件数に比例して実行時間がどんどんと大きくなってしまいます。

N+1問題がある処理
ms, _ := models.Articles(
	models.ArticleWhere.Title.EQ(null.StringFrom("title1")),
).All(ctx, a.db)

for _, m := range ms {
	a, _ := m.Author().One(ctx, a.db)
	fmt.Printf("author_name=%s", a.Name)
}

Eager Loadingを使うと、レコード数が増えても追加で実行されるクエリは一度だけで済むので、N+1問題を回避できます。

Eager LoadingでN+1を回避する
ms, _ := models.Articles(
	models.ArticleWhere.Title.EQ(null.StringFrom("title1")),
	qm.Load("Author"),
).All(ctx, a.db)

for _, m := range ms {
	fmt.Printf("author_name=%s", m.R.Author.Name)
}

デバッグ出力

アプリケーション内の任意の箇所で、boil.DebugModeのフラグを立てることで、実際に実行されたSQL文をログや標準出力で確認することが可能です。

デバッグ設定
boil.DebugMode = true

ms, _ := models.Articles(
	qm.Where("title=?", "title1"),
	qm.Load("Author"),
).All(ctx, a.db)

boil.DebugMode = false

このようにフラグを操作すると、以下のように出力されます。

デバッグ出力
SELECT * FROM `articles` WHERE (title=?);
[title1]
SELECT * FROM `authors` WHERE (`authors`.`id` IN (?));
[1]

筆者が運用しているアプリケーションでは、開発環境のみデバッグ出力を有効にして、生成されるSQLを確認できるようしています。

Raw Query

自分で書いたSQL文を直接実行することもできます。

SQLの直接実行
_, err := queries.Raw("truncate tmp_logs").ExecContext(ctx, db)

クエリの結果を構造体に読み込ませることもできます。

クエリの結果を構造体に読み込ませる
type Summary struct {
	Max int     `boil:"max"`
	Avg float32 `boil:"avg"`
}

func MakeSummary(ctx context.Context, db *sql.DB) (*Summary, error) {
	var s *Summary

	sql := "SELECT SUM(cnt) AS sum, AVG(cnt) AS avg FROM " +
		" ( SELECT author_id, COUNT(*) AS cnt FROM articles GROUP BY author_id ) AS s"
	err := queries.Raw(sql).Bind(ctx, db, &s)

	if err != nil {
		return nil, err
	}

	return s, nil
}

タグ付けした構造体をBind関数に渡すことで、クエリの結果を任意の構造体に読み込ませることができます。Bind関数が受け取る型はinterface{}型であり、実行時にリフレクションが使われます。型を適切に使う方が安全なため、基本はテーブルからコード生成した構造体を使ってシンプルに実装すべきですが、集計などで処理が複雑になった場合などは、集計のためのテーブルを追加するよりもBind関数を使った方が効率的です。

Hook

Hookを使うことで、生成された構造体の参照/更新/削除のクエリの前後に任意の処理を実行させることができます。

実際に追加されるHookです。

生成されるHook
var articleBeforeInsertHooks []ArticleHook
var articleBeforeUpdateHooks []ArticleHook
var articleBeforeDeleteHooks []ArticleHook
var articleBeforeUpsertHooks []ArticleHook

var articleAfterInsertHooks []ArticleHook
var articleAfterSelectHooks []ArticleHook
var articleAfterUpdateHooks []ArticleHook
var articleAfterDeleteHooks []ArticleHook
var articleAfterUpsertHooks []ArticleHook

実際の処理の前に、AddArticleHook関数を呼ぶことで前処理を追加できます。

Hookを追加する
func (a *Article) Delete(ctx context.Context) error {
	article, err := models.FindArticle(ctx, a.db, 1)

	if err != nil {
		return err
	}

	models.AddArticleHook(boil.BeforeDeleteHook, DeleteDependentsOnArticle)
	if err != nil {
		return err
	}

	_, err = article.Delete(ctx, a.db)

	return nil
}

func DeleteDependentsOnArticle(ctx context.Context, db boil.ContextExecutor, article *models.Article) error {
	_, err := models.ArticleThumbnails(
		models.ArticleThumbnailWhere.ArticleID.EQ(article.ID),
	).DeleteAll(ctx, db)

	if err != nil {
		return err
	}

	return nil
}

Hookを使うことでモデルを呼び出す側はシンプルに実装できますが、その分モデル内の責務が増大してしまいます。トリガーとして追加したい処理がそのモデルでおこなうべきものなのか、ユースケースとしてちがうレイヤーでおこなうべきものなのか検討した上で、必要なポイントでのみ使うのがよいと思います。

nullパッケージ

テーブル定義でNOT NULL制約を付加した文字列は、構造体ではString型として定義されます。
一方、NOT NULL制約を外してNULLを許容した場合、SQLBoilerではnull.String型として書き出されます。標準ライブラリのsql.NullString型ではないので注意です。数値、日時の場合も同様です。

nullパッケージ
m, _ := models.FindArticle(ctx, a.db, 1)s

m.Title = null.StringFromPtr(nil)      // nullをセットします
m.Title = null.StringFrom("new title") // 値をセットします

m.UpdatedAt = null.TimeFrom(time.Now()) // 現在時刻を入れます

Goでは文字列のゼロ値は空文字なので、レコードをnullにしたい場合は、StringFromPtr関数を使ってnilを入れています。

テンプレート

生成されるコードをカスタマイズすることができます。任意のディレクトリにテンプレートファイルを配置し、コマンドのオプションでディレクトリの場所を指定します。

SQLBoilerには、配列の一括Insert機能がありません。しかしテンプレートを自前で追加することで、対応することが可能です。

この場合、templatesディレクトリを追加作成し、99_bulk_insert.go.tplというファイルを追加します。一括Insertのカスタムテンプレートの記述に当たってはSQLBoilerでBulk Insertを実現する方法」という記事が参考になります。

ファイルは辞書順に処理されます。またGoのファイルを生成する場合は拡張子を.go.tplとする必要があります。各ファイルはテーブル単位で都度実行されますが、共通して使う関数など、一度だけ生成したい場合は、templatesディレクトリの中に、singletonディレクトリを配置して、その中にファイルを追加します。テンプレートエンジンはtext/templateが使われます。

生成コマンドにtemplatesオプションを追加して再実行します。

sqlboiler mysql \
-c config.toml \
-o models \
-p models \
--no-tests \
--wipe \
--templates ${GOPATH}/pkg/mod/github.com/volatiletech/sqlboiler/v4@v4.6.0/templates,templates

templatesオプションはデフォルトのテンプレートも上書きするので、オリジナルのテンプレートの場所も指定しておく必要があります。

まとめ

SQLBoilerは機能的に非常にシンプルで、Goの特性にマッチしたライブラリです。
またコード生成型のORMライブラリであるため、非常に高速でかつ見通しのよいところも魅力です。

コード生成型のORMライブラリとしては、他にもFacebookによるentがあります。entはマイグレーションやGraphQLとの統合も機能として備えており、また複雑なリレーションの表現も容易です。個人的には多機能すぎるように感じており、特にマイグレーションは別でおこないたいので、SQLBoilerの方が使い勝手がよいのですが、もう少し試してみて気が向いたら、いつか記事にまとめてみたいと思います。

本記事が少しでも多くの人にSQLBoilerを使っていただくきっかけになれば幸いです。

Discussion