【Golang】GORMでUUIDv6/7/8をPrimaryKeyとして使う
この記事では、GORMでUUIDv6/7/8をプライマリキーとして使いたい方向けに、カスタムデータ型の定義方法を紹介します。
UUIDv6/7/8とは
UUIDv4はその完全なランダム性から、MySQL(InnoDB)でプライマリキーとして使用する際にパフォーマンスの問題を引き起こすことがあります。
これを解決するため、ULIDやSnowflakeのような時系列でソート可能なIDジェネレーターがありますが、新たに標準化が進むUUIDv6/7/8も同様に時系列ソート可能な構造を持ち、UUIDのパフォーマンス問題を回避します。
これらのバージョンはタイムスタンプの形式が異なり、v6はグレゴリオ歴、v7はUNIXTIME、v8はカスタムタイムスタンプとなっています。基本的にはUNIXTIMEなUUIDv7を使うことになると思います。
GolangでUUIDを使用する
MySQL 8.20(記事作成時点の最新バージョン)ではまだUUIDv6/7/8に対応していないため、UUIDの生成はGorm側で行う必要があります。
GolangでこれらのUUIDに対応する主要なライブラリはgoogle/uuid
とgofrs/uuid
です。どちらもあまり使用感は変わりませんが、今回はより多くのスターを持つgofrs/uuid
を使用します。
GORMでカスタムデータ型を定義する
GORMでカスタムデータ型を使ってDBとやり取りするには以下のインターフェイスを実装する必要があります。
- Valuers
- カスタムデータ型をデータベースに保存できる形にする
- Scanners
- データベースから値を読み取りカスタムデータ型に戻す
- GormDataTypeInterface
- GORMがデータ型を認識するためのインターフェイス
実装例
以下のコードは、UUIDをカスタムデータ型としてGORMで使用するための基本的な実装です。
この例では、UUID[T]型を定義し、GORMのデータ型としてbinary(16)を返すようにしています。
ジェネリクスはあってもなくても構いません。あるとモデル固有のID型となり複数のIDを扱う場合の開発効率が上がります。
package value
import (
"database/sql/driver"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
type UUID[T any] uuid.UUID
func (u *UUID[T]) GormDataType() string {
return "binary(16)"
}
func (u *UUID[T]) GormDBDataType(db *gorm.DB, field *schema.Field) string {
return "binary"
}
func (u *UUID[T]) Scan(value any) (err error) {
bytes, ok := value.([]byte)
if !ok {
return errors.New("cannnot scan uuid")
}
parseByte, err := uuid.FromBytes(bytes)
*u = UUID[T](parseByte)
return
}
func (u UUID[T]) Value() (bytes driver.Value, err error) {
bytes, err = uuid.UUID(u).MarshalBinary()
return
}
func (u UUID[T]) String() string {
return uuid.UUID(u).String()
}
モデルでこのIDを使用する際は、新たにUUIDv7が生成されるように設定します。以下はModelという共通のベース型を作成し、GORMのBeforeCreateフックを使用してデータ作成時にUUIDを生成する例です。
package model
type Model[T any] struct {
ID value.UUID[T] `gorm:"type:binary(16);<-:create"`
}
func (m *Model[T]) BeforeCreate(db *gorm.DB) (err error) {
uid := uuid.Must(uuid.NewV7())
m.ID = UUID[T](uid)
return
}
Model[T any]
を使用することで、モデルごとに固有のUUID型を持つことができます。<-:createタグは、作成時にのみIDの書き込みを許可し、誤ってIDを上書きすることを防ぎます。
BeforeCreate
はgormのHooksという機能により、Model
型がCreate
されたとき、クエリの実行前にこの関数が発火します。
注意点
Modelを継承するモデルでBeforeCreateを再定義する場合は、継承先のBeforeCreate
で処理が上書きされてしまうため、Model.BeforeCreate
が発火しなくなってしまいます。
そのため、Model.BeforeCreateを明示的に呼び出すことで、UUIDの自動生成を保証する必要があります。
type User struct {
Model[User]
Name string `gorm:"type:varchar(25)"`
Age int
}
func (u *User) BeforeCreate(db *gorm.DB) (err error) {
// なんか処理
err = u.Model.BeforeCreate(db)
return
}
さいごに
この記事ではGORMを使用してUUIDv6/7/8をプライマリキーとして扱う方法について簡単にまとめました。
UUIDv6/7/8は、ランダム性を保ちつつ時系列でソート可能な特性を持っているため、データベース設計において非常に便利です。特に、分散システムや大規模なアプリケーションでの利用を考えると、その利点はさらに明確になります。
今回紹介した方法を用いることで、MySQLなどのRDBをバックエンドに持つアプリケーションで、効率的かつ安全にUUIDをプライマリキーとして利用することが可能になります。
Discussion