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進数の文字列に変換して文字数を縮めたいので、シリアライズの処理もメソッドとして定義します。
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種類作りました。
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"`
}
実際に使うモデルに上の構造体を埋め込んで使います。
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のカラムにセットします。
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()
の仕様は下記を参考にしてください。