独自拡張した型のスライスをgorm+PostgreSQLで使う
独自拡張した型とは
筆者のユースケースの一例として、単一のUUID型、すなわちuuid.UUIDを単純に利用したモデルでは、本来ユーザーIDが入るべき場所にコンテンツIDが入ってしまうなどの問題が発生することがあります。
この問題を解決するため、uuid.UUIDを独自に拡張し、value.UserIDのような形で値オブジェクトを作ることができます。これにより、型の安全性を向上させることができます。以下はその実装例です。
package value
import (
"github.com/google/uuid"
)
type UserID uuid.UUID
var _ ID = (*UserID)(nil)
func (v UserID) IsNil() bool {
return uuid.UUID(v) == uuid.Nil
}
func (v UserID) String() string {
return uuid.UUID(v).String()
}
func (v UserID) Value() (driver.Value, error) {
return uuid.UUID(v).Value()
}
func (v *UserID) Scan(value interface{}) error {
return (*uuid.UUID)(v).Scan(value)
}
独自拡張したUUID型を利用するにあたっての問題
独自拡張した型を利用する際、value.UserIDのスライス型をGormのフィールドとして利用した場合、スライスとしてのValue()やScan()メソッドがないためエラーとなることがあります。以下はその一例です。
type Campaign struct {
ID value.CampaignID `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id" example:"12345678-1234-1234-1234-000000000001" validate:"required"`
UserIDs []value.UserIDs `json:"userId" example:"12345678-1234-1234-1234-000000000001" validate:"required" gorm:"type:uuid[]"` // キャンペーン対象のユーザID
}
→ 保存時に以下のエラーが発生します。
pq: malformed array literal: "cd3d7c53-4d32-4bc6-ac55-3c0d0534457e"
また、スライス型をGormで利用する際に、gormタグのtype指定がないと「got unsupported data type」というエラーが発生しますので注意して下さい。
got unsupported data type: &[]: Table not set, please set it like: db.Model(&user) or db.Table("users")
解決方法
pq.StringArrayなどの実装を参考にして、スライス型専用のValue()とScan()を用意することで解決できます。
参考: https://gorm.io/ja_JP/docs/data_types.html#Scanner-x2F-Valuer
package value
import (
"database/sql/driver"
"github.com/google/uuid"
"github.com/lib/pq"
)
type UserIDs []UserID
func (d UserIDs) Value() (driver.Value, error) {
// UserIDsが空の場合、nilを返してPostgreSQLにNULLとして扱わせる
if len(d) == 0 {
return nil, nil
}
// UUIDの文字列のスライスを作成
uuids := make([]string, len(d))
for i, id := range d {
uuids[i] = uuid.UUID(id).String()
}
// pq.Arrayを使用して、uuidsスライスをPostgreSQLのuuid[]型に変換
return pq.Array(uuids).Value()
}
func (d *UserIDs) Scan(src interface{}) error {
// PostgreSQLのuuid[]型を受け取るための文字列スライス
var uuidStrings []string
// pq.Arrayを使用して、srcを文字列スライスにスキャン
if err := pq.Array(&uuidStrings).Scan(src); err != nil {
return err
}
// 文字列スライスをUserIDs型に変換
*d = make(UserIDs, len(uuidStrings))
for i, str := range uuidStrings {
id, err := uuid.Parse(str)
if err != nil {
return err
}
(*d)[i] = UserID(id)
}
return nil
}
type Campaign struct {
ID value.CampaignID `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id" example:"12345678-1234-1234-1234-000000000001" validate:"required"`
UserIDs value.UserIDs `json:"userId" example:"12345678-1234-1234-1234-000000000001" validate:"required" gorm:"type:uuid[]"` // キャンペーン対象のユーザID
}
あとがき
データベースのカラムで独自拡張した型の配列を利用したいというユースケースは少ないかもしれません。
また、データベース設計上、間違いであることケースが多いと思います。しかし、この問題に直面し、解決策を見つけ出すまでに苦労したため、この記事をメモ代わりに記載しました。