Golangのgormで、PrimaryKeyにXIDを使う

[Golangのgormで、PrimaryKeyにUUIDを使う](http://qiita.com/roothybrid7/items/dc58bae01f941e1a72be) っていうのを以前書いたのですが、MySQLの `UUID_SHORT` 使ってみると単純にAutoincrementしているだけのようでした。

他にユニークなIDを生成できる仕組みはないかと探していたところ、[xid](https://github.com/rs/xid) というのを見つけました。

上のやつはどうやらMongo Object IDアルゴリズムをベースにして実装しているようです。サイズもコンパクトでいい感じに見えたので差し替えてみました。

## アプリケーションで使えるようにする

Golangで、組み込み型以外の型を使うには、sql driverのinterfaceを実装する必要があります。

* database/sql.Valuer
* database/sql.Scanner


### 実装

* DBから値を受け取って、Golang側でxid objectに変換
* SQL Driverが受け取れる値に変換
* JSONエンコード・デコード処理(API用) 

JSONデコードにffjsonを使ってますが、標準のjsonパッケージを使っても問題ないでしょう。

```xid.go
package typedef

import (
	"database/sql/driver"
	"errors"

	json "github.com/pquerna/ffjson/ffjson"
	"github.com/rs/xid"
)

// XID redefines a new type to use application.
type XID struct {
	id xid.ID
}

// NewXID creates a new xid.
func NewXID() *XID {
	return &XID{id: xid.New()}
}

// NewXIDFromString creates a XID object to use xid generator.
func NewXIDFromString(str string) *XID {
	id, err := xid.FromString(str)
	if err != nil {
		return nil
	}
	return &XID{id: id}
}

// XIDFromString creates from string representation.
func XIDFromString(str string) (id XID, err error) {
	v, err := xid.FromString(str)
	if err != nil {
		return
	}
	id.id = v
	return
}

// IsZero defines to check whether nil value or not.
func (id XID) IsZero() bool {
	return len(id.id) == 0
}

// EqualString returns both string representation.
func (id XID) EqualString(s string) bool {
	return id.String() == s
}

// String returns xid string representation.
func (id XID) String() string {
	return id.id.String()
}

// Marshalizations

// MarshalJSON returns the encoded JSON string.
func (id XID) MarshalJSON() ([]byte, error) {
	v := id.id
	return []byte(`"` + v.String() + `"`), nil
}

// UnmarshalJSON sets the value that decoded JSON.
func (id *XID) UnmarshalJSON(data []byte) (err error) {
	var v string
	if err = json.Unmarshal(data, &v); err != nil {
		return
	}
	if err = id.Scan(v); err != nil {
		return
	}
	return
}

// Sql driver interface.

// Scan casts a value to xid.ID
// XXX: DBからのvalueは、[]uint8で渡ってくるっぽい(ascii codeが入ってる)
func (id *XID) Scan(value interface{}) (err error) {
	switch v := value.(type) {
	case string:
		id.id, err = xid.FromString(v)
	case []uint8:
		b := make([]byte, len(v))
		for i, a := range v {
			b[i] = byte(a)
		}
		id.id, err = xid.FromString(string(b))
	default:
		err = errors.New("xid.Scan: invalid value")
	}
	return
}

// Value exports the casted object value.
func (id XID) Value() (driver.Value, error) {
	return id.String(), nil
}
```

## Gormモデル定義

XIDをPrimary keyで使うには、タグでカラムの型をきちんと定義する必要があります。


```model.go
// Model is a base model that defines a primary key and timestamp on operation.
type Model struct {
	XID       T.XID          `gorm:"type:varchar(20);primary_key" json:"id"` // XXX: XID on create callback.
	CreatedAt T.ResourceTime `json:"createdAt"`
	UpdatedAt T.ResourceTime `json:"updatedAt"`
}

// SoftModel is a base model to delete a record.
type SoftModel struct {
	XID       T.XID          `gorm:"type:varchar(20);primary_key" json:"id"` // XXX: XID on create callback.
	CreatedAt T.ResourceTime `json:"createdAt"`
	UpdatedAt T.ResourceTime `json:"updatedAt"`
	DeletedAt *time.Time     `json:"deletedAt,omitempty"` // XXX: ポインターだとカラム追加できない
}


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

## レコード追加時に、XIDを発行し保存

```repos.go
package repos

import (
	"github.com/jinzhu/gorm"

	T "bitbucket.org/team1/app/module/typedef"
	"bitbucket.org/team1/app/module/util"
)

// Gorm callbacks

func updateXIDWhenCreate(scope *gorm.Scope) {
	if !scope.HasError() {
		scope.SetColumn("XID", *T.NewXID()) // Primary Key
	}
}

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