🙆‍♀️

GORMで使う構造体のフィールドにユーザー定義型を使っていいパターン・使えないパターン

2022/06/11に公開

検証環境

Go: 1.18.3
GORM: 1.23.6

基本的にはユーザー定義型は使用できない

当たり前っちゃ当たり前ですが、独自定義型をGORMは解釈できません。
以下は、usersテーブルに、レコードを追加する例です。

main.go
// Userモデル
// Ageに後述のUserAge型を使用
type User struct {
	gorm.Model
	Name   string
	Email  string
	Age    UserAge
}

// Ageモデル
type UserAge struct {
	Num string
}

func main() {
	...(DBに接続するとこは割愛)
	age := UserAge{Num: "20"}
	user := User{
		Name:   "Gorm Taro",
		Email:  "gorm@example.com",
		Age:    age,
	}
	result := DB.Create(&user)
}

次のようなエラーが返ります。

[error] invalid field found for struct main.User's field Age: define a valid foreign key for relations or implement the Valuer/Scanner interface

英語が読めなのでよくわかりませんが、なんか言ってます。

プリミティブ型をラップしただけのものは使用できる

僕が使えるって言ったのはこういうやつね。

main.go
// Userモデル
// Ageに後述のAge型を使用
type User struct {
	gorm.Model
	Name   string
	Email  string
	Age    UserAge
}

// Ageモデル
type UserAge string // stringをラップしただけの型

公式ドキュメントでこのことに明言している箇所を見つけられなかったが、当たり前だからかな?(書いてるけど僕が見つけれてないだけが濃厚)

Valuer/Scannerを定義してあげると使えるようになる

GORMさんがユーザー定義型を解釈できないというのなら、教えてあげればいいじゃない!
ということで、教えるためのインターフェースがあります。
それが、Valuer/Scannerになります。
詳しくは公式ドキュメントを見ていただければと思うので、ここでは簡単に。
https://gorm.io/ja_JP/docs/data_types.html
Valuer  => DB書き込み時に変換する
Scanner => 読み込み時に変換する

変換の仕方を書いてあげましょう。

main.go
type User struct {
	gorm.Model
	Name  string
	Email string
	Age   UserAge
}

// こいつに対してValueとScanを定義してあげる
type UserAge struct {
	Num string `json:"num"`
}

// 書き込み時にstringに変換する
func (u UserAge) Value() (driver.Value, error) {
	bytes, err := json.Marshal(u)
	if err != nil {
		return nil, err
	}
	return string(bytes), nil
}

// 読み出し時に変換する
func (u *UserAge) Scan(input interface{}) error {
	switch v := input.(type) {
	case string:
		return json.Unmarshal([]byte(v), u)
	case []byte:
		return json.Unmarshal(v, u)
	default:
		return fmt.Errorf("unsupported type: %T", input)
	}
}

これさえ設定してあげれば、ユーザー定義型を普通に使えます。
ちなみにDBにはこんなふうに入ります。

+----+----------------------------------------------------+
| id | ... | name      | email            | age           |
+----+----------------------------------------------------+
|  1 | ... | Gorm Taro | gorm@example.com | {"num":"2020"}|
+----+----------------------------------------------------+

せっかくなんで、読み込みの方もチェックしましょう。

main.go
user := User{}
result := DB.First(&user)
if result.Error != nil {
	panic(result.Error)
}
fmt.Printf("user: %+v\n", user1)
fmt.Printf("age: %T\n", user1.Age)
user: ... Name:Gorm Taro Email:gorm@example.com Age:{Num:2020}}
name: string
age: main.UserAge // 読み込んだだけで、目的の型に変換された!

正直こんなん書くのだるいと思う。

まとめ

ActiveRecordが恋しい

Discussion