概要
この記事を書いている(2019/12/07)時点では、SQLBoilerに、Bulk Insert は実装されていません。
issueやPull Requestはいくつか上がっているようですが、内部に取り込まれる様子がありません。
おそらくその理由は、MySQL、Postgresなどの各種DBにうまく対応しつつ、Insertした複数のstructにIDを埋める、という用件を満たせないからだと推測されます。
(SQLBoilerでは、通常のInsertではInsert後にlast_insert_id
を用いて、structのIDにマッピングするように実装されています)
Bulk Insert が実装されていないORMは他にもあり、それらもほぼ同様の理由で導入が見送られていそうな気がします。
しかし、実務では、Bulk Insert を使いたい場面は多々発生します。
どうにかうまく、実現できる方法はないでしょうか?
先ほど、Bulk Insertが実装されない理由を述べましたが、裏を返せば、そこを妥協すれば実装可能なわけです。
つまり、実務レベルでは、「使っているDBに実装を合わせつつ、structのIDにマッピングされなくてもよい」という風に割り切ることもできます。
SQLBoilerには、コード生成のテンプレートに使うファイルを変更する機能があります。その機能を使い、Bulk Insert用のテンプレートファイルを作ることでこの問題を解決することができます。
今回は、MySQLを使用することを前提とした場合で進めていきます。
SQLBoilerのテンプレート変更
テンプレートファイルの作成
まず、Bulk Insert用の関数の元になるテンプレートファイルを作成します。
作業ディレクトリ配下に、templates
ディレクトリを作成し、その中に、15_bulk_insert.go.tql
という名前でテンプレートを保存します。
(この時のディレクトリ名とファイル名は適宜変更可能ですが、コード生成される順番に影響します)
今回は、UpdateAll
, DeleteAll
などの名前に合わせて、InsertAll
という関数名にします。
{{- $alias := .Aliases.Table .Table.Name -}}
{{- $schemaTable := .Table.Name | .SchemaTable}}
// InsertAll inserts all rows with the specified column values, using an executor.
func (o {{$alias.UpSingular}}Slice) InsertAll({{if .NoContext}}exec boil.Executor{{else}}ctx context.Context, exec boil.ContextExecutor{{end}}, columns boil.Columns) error {
ln := int64(len(o))
if ln == 0 {
return nil
}
var sql string
vals := []interface{}{}
for i, row := range o {
{{- template "timestamp_bulk_insert_helper" . }}
{{if not .NoHooks -}}
if err := row.doBeforeInsertHooks(ctx, exec); err != nil {
return err
}
{{- end}}
nzDefaults := queries.NonZeroDefaultSet({{$alias.DownSingular}}ColumnsWithDefault, row)
wl, _ := columns.InsertColumnSet(
{{$alias.DownSingular}}AllColumns,
{{$alias.DownSingular}}ColumnsWithDefault,
{{$alias.DownSingular}}ColumnsWithoutDefault,
nzDefaults,
)
if i == 0 {
sql = "INSERT INTO {{$schemaTable}} " + "({{.LQ}}" + strings.Join(wl, "{{.RQ}},{{.LQ}}") + "{{.RQ}})" + " VALUES "
}
sql += strmangle.Placeholders(dialect.UseIndexPlaceholders, len(wl), len(vals)+1, len(wl))
if i != len(o)-1 {
sql += ","
}
valMapping, err := queries.BindMapping({{$alias.DownSingular}}Type, {{$alias.DownSingular}}Mapping, wl)
if err != nil {
return err
}
value := reflect.Indirect(reflect.ValueOf(row))
vals = append(vals, queries.ValuesFromMapping(value, valMapping)...)
}
if boil.DebugMode {
fmt.Fprintln(boil.DebugWriter, sql)
fmt.Fprintln(boil.DebugWriter, vals...)
}
{{if .NoContext -}}
_, err := exec.Exec(ctx, sql, vals...)
{{else -}}
_, err := exec.ExecContext(ctx, sql, vals...)
{{end -}}
if err != nil {
return errors.Wrap(err, "{{.PkgName}}: unable to insert into {{.Table.Name}}")
}
return nil
}
{{- define "timestamp_bulk_insert_helper" -}}
{{- if not .NoAutoTimestamps -}}
{{- $colNames := .Table.Columns | columnNames -}}
{{if containsAny $colNames "created_at" "updated_at"}}
{{if not .NoContext -}}
if !boil.TimestampsAreSkipped(ctx) {
{{end -}}
currTime := time.Now().In(boil.GetLocation())
{{range $ind, $col := .Table.Columns}}
{{- if eq $col.Name "created_at" -}}
{{- if eq $col.Type "time.Time" }}
if row.CreatedAt.IsZero() {
row.CreatedAt = currTime
}
{{- else}}
if queries.MustTime(row.CreatedAt).IsZero() {
queries.SetScanner(&row.CreatedAt, currTime)
}
{{- end -}}
{{- end -}}
{{- if eq $col.Name "updated_at" -}}
{{- if eq $col.Type "time.Time"}}
if row.UpdatedAt.IsZero() {
row.UpdatedAt = currTime
}
{{- else}}
if queries.MustTime(row.UpdatedAt).IsZero() {
queries.SetScanner(&row.UpdatedAt, currTime)
}
{{- end -}}
{{- end -}}
{{end}}
{{if not .NoContext -}}
}
{{end -}}
{{end}}
{{- end}}
{{- end -}}
sqlboiler.tomlの変更
sqlboiler.tomlに以下を追加します。
本来のテンプレートもそのまま使う場合には、sqlboiler内部の、templates
, templates_test
を含める必要があります。
templates = [
"/path/to/sqlboiler/templates",
"/path/to/sqlboiler/templates_test",
"templates"
]
コード生成
通常通り、以下のコマンドでコードを生成します。
$ sqlboiler --wipe mysql
すると、新しく、各モデルにInsertAll
関数生成され、以下のサンプルのように使えるようになっているはずです。
slice := models.UserSlice{}
err := slice.InsertAll(...)
最後に
SQLBoilerは、今まで使用したGoのORMの中で最も手になじんだものでした。
RelationもGormなどに比べて直感的に書け、唯一、残念だったのがBulk Insertが無いことでした。
そのデメリットもほぼなくなったことで、個人的にさらにSQLBoilerが心地の良いものになりました。