🔑

【Golang】GORMでUUIDv6/7/8をPrimaryKeyとして使う

2024/03/03に公開

この記事では、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/uuidgofrs/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