GoにおけるORMと、SQLBoiler入門マニュアル
前置き
この文章は技術書典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関数を使ってモデルにデータを読み込むには、次のように実装します。
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
}
構造体に直接読み込むことはできないので、読み込み部分を別途実装する必要があります。
更新系のクエリの場合は次のようにプリペアドステートメントを生成して、実行します。プリペアドステートメントに引数として渡した値は自動でサニタイズされます。
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ライブラリと比較して、もっとも機能が多いもののひとつであり、マイグレーションなども可能です。マイグレーションとは、運用中のデータベースにデータを入れたまま、テーブルを追加したりカラムを変更するなどして、スキーマを管理する機能です。
参照系の実装は次のようになります。
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
}
続いて更新系の記述方法を紹介します。
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
sqlxはdatabase/sql
を拡張したサードパーティのライブラリです。
sqlxにはSQL文を組み立てる機能はないため、厳密にはORMとはいえないかもしれませんが、ORMの代替として非常によく使われており、比較のために紹介します。
sqlxがdatabase/sql
と大きく違う点は、Scan関数で構造体を使用することができることです。
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関数
を使うことで、見通しのよい更新処理を実装することができます。
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
というパッケージ名でコード生成されたものを、
自前の構造体に埋め込んで使用します。
import (
"context"
"database/sql"
"github.com/volatiletech/sqlboiler/v4/boil"
"sample/models"
)
type Article struct {
*models.Article
db *sql.DB
}
参照の例です。
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
}
更新処理です。
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句の場合、コード生成された関数を使ってクエリを組み立てることもできます。型の安全性の点からメリットが大きいので、積極的に使っていきたいです。
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関数を使用します。返り値は構造体のスライスです。
ms, err := models.Articles(
models.ArticleWhere.AuthorID.EQ(1),
).All(ctx, a.db)
プライマリキーで取得する場合は、構造体名を後置した、Find関数を利用することもできます。
m, err := models.FindArticle(ctx, a.db, 1)
SELECTクエリを実行してデータが存在しない場合は、エラーとしてsql.ErrNoRows
が返ります。
Enums
テーブルで定義した列挙型は定数として生成されます。
const (
ArticlesStatusDRAFT = "DRAFT"
ArticlesStatusSUBMITTED = "SUBMITTED"
ArticlesStatusREVIEWED = "REVIEWED"
ArticlesStatusPUBLISHED = "PUBLISHED"
)
そのため、コード内でテーブル内のenum値を二重定義する必要はありません。テーブルからコード生成された定数を、そのままモデルにセットすることができます。
m.Status = models.ArticlesStatusPUBLISHED
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のWhereIn
、AndIn関数
が取る引数はinterface{}
型の配列です。したがって、数値型の場合はinterface{}
型に変換してから渡す必要があります。ここでは変換のためにInts64ToInterfaces関数
を追加しました。
func Ints64ToInterfaces(nums []int64) []interface{} {
results := make([]interface{}, len(nums))
for i, n := range nums {
results[i] = n
}
return results
}
Count
コード生成された構造体では、Count関数も利用可能です。任意の条件に該当するレコードの数を調べることができます。
count, err := models.Articles(
models.ArticleWhere.Status.EQ(models.ArticlesStatusPUBLISHED),
).Count(ctx, a.db)
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
を使用します。この場合指定しなかったフィールドは更新されません。
CreatedAt
とUpdatedAt
は通常指定しなくても、自動的に更新されますが、boil.Whitelist
を指定した場合は更新されません。
err := article.Insert(ctx, a.db, boil.Whitelist("Title", "AuthorID"))
boil.Blacklist
を指定した場合は、基本的にboil.Infer
と同じ挙動ですが、指定したフィールドは除外します。boil.Graylist
を指定した場合、boil.Infer
と同じ挙動に加えて、指定したフィールドを更新対象に加えます。
Update
すでに存在するレコードの更新もInsertと似ています。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です。
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文を実行してくれる関数です。
author := &models.Author{
Name: "gami",
}
author.Upsert(ctx, a.db, boil.Infer(), boil.Infer())
第3引数はUpdate用、第4引数はInsert用のオプションです。Upsertの処理はデータベースエンジンによってSQL文の仕様が異なるため、使用するデータベースに応じて関数のインターフェースが異なります。今回はMySQLの例です。
Delete
Delete文を実行して、構造体を削除します。
m, err := models.FindArticle(ctx, a.db, 1)
if err != nil {
return err
}
_, err = m.Delete(ctx, a.db)
最初の返り値は削除された行数です。対象の行のみを削除するので、Eager Loading
された構造体を削除することはありません。
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関数を使って再読み込みすることができます。
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関数を追加します。
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が存在する場合、件数に比例して実行時間がどんどんと大きくなってしまいます。
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問題を回避できます。
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文を直接実行することもできます。
_, 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です。
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関数を呼ぶことで前処理を追加できます。
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
型ではないので注意です。数値、日時の場合も同様です。
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