GORMでPostgresのjsonb型をシンプルな実装で扱う方法についてご紹介します。
Postgresのjsonb型
PostgresではJSONを扱うデータ型としてjson型の他にjsonb型が用意されています。jsonb型の方が下記の通り、公式でも利用が推奨されていてデータ取得時のパフォーマンスが良くなるなどのメリットが有るようです。
json型とjsonb型というデータ型は、ほとんど 同一の入力値セットを受け入れます。 現実的に主要な違いは効率です。 jsonデータ型は入力テキストの正確なコピーで格納し、処理関数を実行するたびに再解析する必要があります。 jsonbデータ型では、分解されたバイナリ形式で格納されます。 格納するときには変換のオーバーヘッドのため少し遅くなりますが、処理するときには、全く再解析が必要とされないので大幅に高速化されます。 また jsonb型の重要な利点はインデックスをサポートしていることです。
一般的に、ほとんどのアプリケーションではJSONデータ型としてjsonb型のほうが望ましいでしょう。ただし、オブジェクトキーを従来のような順序であることを仮定する非常に特殊なニーズが存在するような場合は除きます。
GORM公式でのjsonbのサンプル実装
GORMでjsonbを使おうと思ったときに、公式のサンプル実装は以下のようになっています(2024/6/26現在)
type JSON json.RawMessage // Scan scan value into Jsonb, implements sql.Scanner interface func (j *JSON) Scan(value interface{}) error { bytes, ok := value.([]byte) if !ok { return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value)) } result := json.RawMessage{} err := json.Unmarshal(bytes, &result) *j = JSON(result) return err } // Value return json value, implement driver.Valuer interface func (j JSON) Value() (driver.Value, error) { if len(j) == 0 { return nil, nil } return json.RawMessage(j).MarshalJSON() }
この実装はjson.RawMessage
型を用いる方式で、実際にデータを取り出すには更にパース処理が必要になります。可変のJSONの場合には利点がありますが、固定の形式のJSONを扱う場合には、ちょっと扱いずらそうです。
シンプルな実装例
以下のような、ユーザーがロールIDのスライス
とレジュメの構造体
を保持するデータを例にします。
type User struct {
gorm.Model // これはGORMのおまじない(id,created_at,updated_at,deleted_atを良しなに処理してくれる)
Name string
RoleIDs RoleIDs // ここをjsonbで扱う
Resume Resume // ここをjsonbで扱う
}
type RoleID int
type RoleIDs []RoleID
type Resume struct {
Summary string
Experiences []string
Skills []string
}
DBスキーマはこんな感じです。
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
role_ids jsonb NOT NULL DEFAULT '[]',
resume jsonb NOT NULL DEFAULT '{}',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP
);
jsonbで扱う型のValue()関数
とScan()関数
を実装します。
これらの関数はGORMが良しなに呼んでくれます。
-
Value()関数
は、保存や更新時に呼ばれます。 -
Scan()関数
は、取得時に呼ばれます。
- RoleIDs
// RoleIDs Value Marshal func (ids RoleIDs) Value() (driver.Value, error) { return json.Marshal(ids) } // RoleIDs Scan Unmarshal func (ids *RoleIDs) Scan(value interface{}) error { b, ok := value.([]byte) if !ok { return errors.New("User: type assertion to []byte failed") } return json.Unmarshal(b, &ids) }
- Resume
// Resume Value Marshal func (resume Resume) Value() (driver.Value, error) { return json.Marshal(resume) } // Resume Scan Unmarshal func (resume *Resume) Scan(value interface{}) error { b, ok := value.([]byte) if !ok { return errors.New("Resume: type assertion to []byte failed") } return json.Unmarshal(b, &resume) }
実装するのはこれだけです。簡単ですよね。
RoleIDs
とResume
の実装は型の違いだけで中身は全く同じです。
動かしてみる
上記の型を用いてデータを挿入し、その挿入したデータを取得してログ出力する実装です。
func main() {
dsn := "host=db user=postgres password=passw0rd dbname=postgres sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
// INSERT
user := User{
Name: "Fujiwara Takumi",
RoleIDs: RoleIDs{1, 2, 3},
Resume: Resume{
Summary: "Hogehoge",
Experiences: []string{
"Experience 1",
"Experience 2",
},
Skills: []string{
"Skill 1",
"Skill 2",
},
},
}
result := db.Create(&user)
if result.Error != nil || result.RowsAffected != 1 {
log.Println("create user error:", result.Error)
return
}
insertedID := user.ID
// SELECT
var selectedUser User
if err := db.First(&selectedUser, insertedID).Error; err != nil {
log.Println("select user error:", err)
return
}
log.Printf("%+v\n", selectedUser)
}
実行結果
2024/06/26 01:03:55
{
Model:{
ID:5
CreatedAt:2024-06-26 01:03:55.006088 +0000 UTC
UpdatedAt:2024-06-26 01:03:55.006088 +0000 UTC
DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}
}
Name:Fujiwara Takumi
RoleIDs:[1 2 3]
Resume:{
Summary:Hogehoge
Experiences:[Experience 1 Experience 2]
Skills:[Skill 1 Skill 2]
}
}
*インデントは若干加工してます
保存と取得がちゃんと動いてることを確認できました🤓
今回使ったコード等はこちらに置いてます。