LoginSignup
22
21

More than 5 years have passed since last update.

Golangのgormで、PrimaryKeyにUUIDを使う

Last updated at Posted at 2015-08-12

gormという、ORMライブラリを使っています。
タグでprimarykeyやカラムの定義できるし、論理削除もできるすぐれものですが、
Primarykeyの型をintなどにしてしまうと、Auto increment固定なのが困るところです。

// gorm.Model
//type Model struct {
//    ID        uint64 `gorm:"primary_key"`
//    CreatedAt time.Time
//    UpdatedAt time.Time
//    DeletedAt *time.Time
//}

type User struct {
    gorm.Model
    Name string
}

DBに保存された順番をIDとして採番したくなくて、UUIDやTwitterのSnowflakeとかいろいろ調べたのですが、UUIDでは128ビット必要で長すぎ、別のミドルウエアが必要になるものは使いたくないので、いろいろ調べた結果たどり着いたのが、MySQLの UUID_SHORT() 関数です。

やりたいことは単純に下記の3つでした。

  • レコードを追加するときにIDを発行
  • DBに格納するときは、数値で格納(UUID_SHORTそのままの値)
  • (おまけ)クライアントにデータを返すときには、IDは16進数の文字列に変換し縮めて返す。

UUID独自のタイプを定義

Golangでは、type キーワードを使って組み込み型を再定義?できるので、UUIDという独自タイプを定義します。
DBに格納するときは、uint64 なんですが、クライアントがアクセスするURLや返すデータとしては 16進数の文字列に変換して文字数を縮めたいので、シリアライズの処理もメソッドとして定義します。

uuid.go
package typedef

import (
    "errors"
    "strconv"
)

// UUIDは、RDBではuint64で保存するが、
// APIのレスポンスやRedisでは16進数の文字列に変換して返すためのType
type UUID uint64

func (u UUID) IsZero() bool {
    return u == 0
}

func (u UUID) MarshalJSON() ([]byte, error) {
    hex := UintToHex(u)
    if hex != "" {
        return []byte(`"` + UintToHex(u) + `"`), nil
    }
    return nil, errors.New("UUID.MarshalText: invalid value")
}

// TODO: 多分タブルクオートを先にトリミングする必要がある
func (u *UUID) UnmarshalJSON(data []byte) (err error) {
    i := UUID(UintFromHex(string(data)))
    u = &i
    return
}

func (u UUID) MarshalText() ([]byte, error) {
    hex := UintToHex(u)
    if hex != "" {
        return []byte(UintToHex(u)), nil
    }
    return nil, errors.New("UUID.MarshalText: invalid value")
}

func (u *UUID) UnmarshalText(data []byte) (err error) {
    i := UUID(UintFromHex(string(data)))
    u = &i
    return
}

func (u UUID) ToHex() string {
    return strconv.FormatUint(uint64(u), 16)
}

// MARK: Helpers

func UintToHex(i interface{}) string {
    switch v := i.(type) {
    case uint64:
        return strconv.FormatUint(v, 16)
    case UUID:
        return strconv.FormatUint(uint64(v), 16)
    }
    return ""
}

func UintFromHex(hex string) uint64 {
    if num, err := strconv.ParseUint(hex, 16, 64); err != nil {
        return 0
    } else {
        return num
    }
}

モデル定義の構造体を作る

Golangでは構造体に構造体を埋め込めるらしいので、IDのカラムを定義したベースのモデルの構造体を作りました。
IDだけ持ったもの、時間付きのもの、論理削除用(gormで定義されているものとほぼ同じもの)の3種類作りました。

model.go
type ModelUUID struct {
    UUID UUID `gorm:"primary_key"` // XXX: UUID_SHORT() on create callback.
}

type Model struct {
    UUID      UUID      `gorm:"primary_key"` // XXX: UUID_SHORT() on create callback.
    CreatedAt time.Time `json:"createdAt"`
    UpdatedAt time.Time `json:"updatedAt"`
}

type SoftModel struct {
    UUID      UUID       `gorm:"primary_key"` // XXX: UUID_SHORT() on create callback.
    CreatedAt time.Time  `json:"createdAt"`
    UpdatedAt time.Time  `json:"updatedAt"`
    DeletedAt *time.Time `json:"deletedAt,omitempty"`
}

実際に使うモデルに上の構造体を埋め込んで使います。

user.go
type User struct {
    //gorm.Model        // see: https://github.com/jinzhu/gorm#conventions
    SoftModel
    Username       string          `sql:"size:180;not null;index" json:"username"`

レコード追加時にUUIDを発行しDBに保存する。

gormでは、新規作成、更新、削除時などにコールバックを使えるので、それを利用します。
リレーションオブジェクトが保存される前だったらどこでもよいと思うのですが、モデルのバリデーションが実行されたあとにしました。
UUID_SHORT() 関数の実行結果を取得して、PrimaryKeyのカラムにセットします。

model.go
var (
    reqKey        private
    UUIDGenerator = func(db *gorm.DB) UUID {
        var uuid uint64
        db.Unscoped().Raw("SELECT UUID_SHORT()").Row().Scan(&uuid)
        return UUID(uuid)
    }
)

// MARK: Gorm callbacks

func UpdateUUIDWhenCreate(scope *gorm.Scope) {
    if !scope.HasError() {
        scope.SetColumn("UUID", UUIDGenerator(scope.DB())) // Primary Key
    }
}

func init() {
    // XXX: DBのtimestampをUTCにする
    gorm.NowFunc = util.NowUtcFunc
gorm.DefaultCallback.Create().Before("gorm:save_before_associations").Register("myapp:update_uuid_when_create", UpdateUUIDWhenCreate)
}

こんな感じで、DBに格納された順じゃないIDを発行することができました。

UUID_SHORT() の仕様は下記を参考にしてください。

22
21
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
22
21