GORMで使うmodelの構造体をprivateにできるか

GORMで利用する構造体を下記のような形から

type Hogehoge struct {
	ID        int       `gorm:"id"`
	Hoge      string    `gorm:"hoge"`
	CreatedAt time.Time `gorm:"created_at"`
	UpdatedAt time.Time `gorm:"updated_at"`
}

次のような形にして、スコープを狭めることができないか調査しました。他の層で間違って参照されないように、スコープを狭めるのが狙いです。

type hogehoge struct {
	id        int       `gorm:"id"`
	hoge      string    `gorm:"hoge"`
	createdAt time.Time `gorm:"created_at"`
	updatedAt time.Time `gorm:"updated_at"`
}

結論

少し思っていたのとはちがいますが、下記のように定義することで、privateな構造体として定義しつつGORMでも利用できることがわかりました。

type hogehoge struct {
	ID        int       `gorm:"id"`
	Hoge      string    `gorm:"hoge"`
	CreatedAt time.Time `gorm:"created_at"`
	UpdatedAt time.Time `gorm:"updated_at"`
}

ご覧の通り、構造体の各フィールドのイニシャルは大文字にする必要があります。

構造体の各フィールドを大文字にしないといけない理由

GORMの処理で、各構造体がexportされているかをチェックする処理が有り、そこで引っかかってしまうためです。

https://github.com/go-gorm/gorm/blob/40f4afe8c21d96db63174bd501fb61d6e73c5587/schema/schema.go#L203C7-L203C7

for i := 0; i < modelType.NumField(); i++ {
	if fieldStruct := modelType.Field(i); ast.IsExported(fieldStruct.Name) {
		if field := schema.ParseField(fieldStruct); field.EmbeddedSchema != nil {
			schema.Fields = append(schema.Fields, field.EmbeddedSchema.Fields...)
		} else {
			schema.Fields = append(schema.Fields, field)
		}
	}
}

GORMにParseWithSpecialTableName()という関数が用意されていて、そこで↑の処理がかかれています。構造体のフィールドの頭文字を小文字にしてしまうと、IsExported()でfalseが返ります。

IsExported()は次のような処理になっていました。

// IsExported reports whether name starts with an upper-case letter.
func IsExported(name string) bool { return token.IsExported(name) }

IsExported()でfalseになると、schema.Fieldsに値が入らないので、最終的に意図した処理ができなくなります。

まとめ

現在担当中のプロダクトをMVCモデルからレイヤードアーキテクチャに書き換えているのですが、その過程での調査です。今回は公式のドキュメントを読んでも分からなかったため、パッケージの中身をブレークポイント張ってデバッグしながら調査しました。良い勉強になりました。