SQLBoilerで任意の型を設定する方法

2024-02-21

はじめに

初めまして。 Belong で Junior Engineer として、主に Backend の開発を担当している、suzu です。 今回は Go 言語の ORM である、SQLBoiler にて任意の型を設定する方法についてまとめています。 詳細な設定方法に関する文献が多くなく、困った部分をまとめたので、参考になれば幸いです。

SQLBoiler とは

データベースのスキーマから ORM を生成できるライブラリです。 https://github.com/volatiletech/sqlboiler

他の ORM に比べた利点として、スキーマからのコード生成が行える為、 自身でスキーマとのマッピング等を考える必要なく、機能の実装に専念できるという点があります。

Belong での利用例

Belong では幾つかのプロダクトにおいて、 SQLBoiler を利用しての開発を行っています。 基本的な処理に関しては SQLBoiler の生成物を利用しつつも、サブクエリが必要な場合等の複雑なクエリに関しては、無理に SQLBoiler に頼らず、Raw Query を書く、という使い方をすることが多いです。 (その際のマッピング先として、 SQLBoiler にて生成された構造体を用いるケースもあります。)

型のオーバーライドについて

今回の主題である、型のオーバーライドについてです。 SQLBoiler では、データベースの型と Go の型の対応をドライバーが推論してくれます。 推論した型と違う型を使用したい場合、 Types1 設定をすることで、ドライバーが推論した型を任意の型でオーバーライドできます。

設定方法

sqlboiler.tomlに以下を記載することで、オーバーライドを設定できます。

  • types.match
    • 元々の要素、及び型の情報
  • types.replace
    • 対象の型
  • types.imports
    • replace で指定した型が定義されているパッケージ

例として MySQL データベースに定義した、テーブルtable_aの要素dateを SQLBoiler を通じて、util.CustomDate型で扱いたい場合を考えましょう。 その場合、以下のように記述できます。

[[types]]
 [types.match]
    type = "null.Time"
    nullable = true
    tables = ["table_a"]
    name = "date"

 [types.replace]
    type = "util.CustomDate"

 [types.imports]
    third_party = ['"github.com/belong-inc/belong-lib/util"']

複数設定

設定したいオーバーライドが複数存在する場合は、以下のように並べて記述します。


[[types]]
[types.match]
    type = "null.Time"
    nullable = true
    tables = ["table_a"]
    name = "date"

[types.replace]
    type = "util.CustomDate"

[types.imports]
    third_party = ['"github.com/belong-inc/belong-lib/util"']

[[types]]
 [types.match]
    type = "null.Time"
    nullable = true
    tables = ["table_a"]
    name = "price"

 [types.replace]
   type = "util.CustomFloat"

 [types.imports]
   third_party = ['"github.com/belong-inc/belong-lib/date"']

同名要素への一括設定

また、複数のテーブルの同名要素に対して、同じ設定をしたい場合は、まとめて設定できます。 (外部キーを持つカラムなどで役立てそうですね。)


[[types]]
  [types.match]
    type = "null.Time"
    nullable = true
    tables = ["table_a","table_b","table_c"]
    name = "date"

  [types.replace]
    type = "util.CustomDate"

  [types.imports]
    third_party = ['"github.com/belong-inc/belong-lib/util"']

オーバーライドする型の実装

SQLBoiler で使用する型には、以下の Interface が実装されている必要があります。

  • driver.Valuer
    • driver.Valueに変換する際のもの
  • sql.Scanner
    • sql.Rows.Scanでの変換時のもの

例として、SQLBoiler で用いられている、 null.Time2 のコードを見てみましょう。 null.Time は、Validフィールドを持ち、Timeフィールドがnilの場合はValidfalseにするという形で nullable な値を表現しています。

// Time is a nullable time.Time. It supports SQL and JSON serialization.
type Time struct {
	Time  time.Time
	Valid bool
}

Scan

Scan は DB からの値を受け取る際のメソッドです。 .(type)で型判定を行い、time.Time型の場合はそのまま代入し、nilの場合はValidfalseにして返します。

// Scan implements the Scanner interface.
func (t *Time) Scan(value interface{}) error {
	var err error
	switch x := value.(type) {
	case time.Time:
		t.Time = x
	case nil:
		t.Valid = false
		return nil
	default:
		err = fmt.Errorf("null: cannot scan type %T into null.Time: %v", value, value)
	}
	t.Valid = err == nil
	return err
}

Value

Value は DB へどのように値を渡すかを定義するメソッドです。 Validfalseの場合はnilを返し、それ以外の場合はtime.Time型の値を返します。

// Value implements the driver Valuer interface.
func (t Time) Value() (driver.Value, error) {
	if !t.Valid {
		return nil, nil
	}
	return t.Time, nil
}

まとめ

今回は、SQLBoiler で任意の型を設定する方法についてまとめました。

Belong ではエンジニアを募集しています。Go に 興味のある方、技術を用いたビジネス課題の解決に興味がある方など、以下リンクをぜひご覧ください!

Footnotes

  1. https://github.com/volatiletech/sqlboiler?tab=readme-ov-file#types

  2. https://github.com/volatiletech/null/blob/master/time.go#L118