0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

独自拡張した型のスライスをgorm+PostgreSQLで使う

独自拡張した型とは

筆者のユースケースの一例として、単一のUUID型、すなわちuuid.UUIDを単純に利用したモデルでは、本来ユーザーIDが入るべき場所にコンテンツIDが入ってしまうなどの問題が発生することがあります。

この問題を解決するため、uuid.UUIDを独自に拡張し、value.UserIDのような形で値オブジェクトを作ることができます。これにより、型の安全性を向上させることができます。以下はその実装例です。

value/uesr.go
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()メソッドがないためエラーとなることがあります。以下はその一例です。

entity/campaign.go
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

value/user_ids.go
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
}
entity/campaign.go
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
}

あとがき

データベースのカラムで独自拡張した型の配列を利用したいというユースケースは少ないかもしれません。
また、データベース設計上、間違いであることケースが多いと思います。しかし、この問題に直面し、解決策を見つけ出すまでに苦労したため、この記事をメモ代わりに記載しました。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?