エンティティの識別子に ULID を使ってみよう
エンティティの識別子の生成タイミング問題
DDD[1]では,一意な識別子を持ち,その識別子によって識別できるモデルをエンティティ (Entity) として表現します.このエンティティの識別子の生成方法には様々な種類がありますが,大きく分けて永続化前に生成する早期生成と永続化後に生成する遅延生成の2種類に分けられます.
エンティティの識別子の生成に遅延生成を用いると本来 Not Null な識別子を Nullbale として扱う必要性が生じてしまいます.これを避けるのであれば,早期生成を採用すると良く,IDDD本[2]などではUUIDやGUIDをアプリケーション側で生成し識別子に用いる例が紹介されています.
しかし,技術的な問題でUUIDなどのランダムな値を識別子として採用しづらいケースも存在します.たとえば,MySQL (InnoDB) ではプライマリキーにランダムな値を用いるとINSERTの効率が落ちてしまいます[3].これを回避するにはエンティティとしての識別子とは別にシーケンシャルなプライマリキーを持つか,データベースにシーケンス用のテーブルを作成し事前採番するか[4]などが必要ですが,どれもしっくりこない (めんどくさい) なと感じていました.
そんな時に,先日見かけたツイート[5]で ULID というものの存在を知り,これは便利だと思ったのでエンティティの識別子として使ってみようと思いました[6].
ULID (Universally Unique Lexicographically Sortable Identifier) とは
ULIDの詳細に関しては仕様[7]も紹介記事[8]もあるので,ここでは踏み込みませんが,簡単に説明すると「生成順でソート可能な識別子」です.1ミリ秒単位で順序が保証されており,同じ1ミリ秒の中でも同じ生成器であれば最大
使ってみよう
Go でサンプルコードを書いてみました.ULIDのライブラリはoklog/ulid[9]です
まずは各識別子の共通部分となる Identifier
と,そこで使われる ULIDGenerator
です.
import (
"crypto/rand"
"github.com/oklog/ulid/v2"
"io"
)
type Identifier struct {
identifier string
}
type IdentifierGenerator interface {
Generate() Identifier
}
var defaultGenerator IdentifierGenerator
func init() {
defaultGenerator = newULIDGenerator(rand.Reader)
}
func GenerateIdentifier() Identifier {
return defaultGenerator.Generate()
}
func NewIdentifier(id string) Identifier {
return Identifier{
identifier: id,
}
}
func (i Identifier) Value() string {
return i.identifier
}
func (i Identifier) Equal(other Identifier) bool {
return i.identifier == other.identifier
}
type ULIDGenerator struct {
entropy *ulid.MonotonicEntropy
}
func newULIDGenerator(reader io.Reader) *ULIDGenerator {
return &ULIDGenerator{
entropy: ulid.Monotonic(reader, 0),
}
}
func (g *ULIDGenerator) Generate() Identifier {
id := ulid.MustNew(ulid.Timestamp(time.Now()), g.entropy)
return Identifier{
identifier: id.String(),
}
}
※ 実際はテストしやすいように time.Now()
をラップしたものを使ってますが省略
-
GenerateIdentifier()
は新規生成用のコンストラクタです.ULIDGnerator
を用いて識別子の早期生成を行います. -
NewIdentifier()
は再構築用のコンストラクタです.リポジトリからエンティティを構築したり,テストでエンティティを構築する際はこちらを使います. -
newULIDGenerator()
で ULID の生成器を作成します.この時乱数を与えて単調増加するエントロピーを作ります.識別子を生成するたびに乱数変えてエントロピーを作り直しちゃうと同一ミリ秒内で識別子が単調増加しないので注意.ulid.Monotonic()
の第2引数は単調増加の最大値を指定するためのもので,0を指定すると1からmath.MaxUint32
がランダムに選ばれるようです.詳しくは godoc [10] を見てください.
次に Identifier
を利用する各エンティティのサンプルです.ここでは MessageId
という識別子しか持たない Message
エンティティを用意しました.
type Message struct {
messageId MessageId
}
func NewMessage() *Message {
return &Message{
messageId: GenerateMessageId(),
}
}
func ReconstructMessage(id string) *Message {
return &Message{
messageId: NewMessageId(id),
}
}
func (m *Message) Id() string {
return m.messageId.Value()
}
func (m *Message) Equal(other *Message) bool {
return m.messageId.Equal(other.messageId)
}
type MessageId struct {
identifier Identifier
}
func GenerateMessageId() MessageId {
return MessageId{
GenerateIdentifier(),
}
}
func NewMessageId(id string) MessageId {
return MessageId{
NewIdentifier(id),
}
}
func (id MessageId) Value() string {
return id.identifier.Value()
}
func (id MessageId) Equal(other MessageId) bool {
return id.identifier.Equal(other.identifier)
}
Message
と MessageId
も Identifier
と同じように新規生成用のコンストラクタと再構築用のコンストラクタを別で用意します. MessageId
を用意せず, Identifier
を直接使うということも可能ですが, MessageId
を使ってあげる方が意図が理解しやすく保守性も高まると思います.
実際に Message
をいくつか新規生成してみます.
import (
"fmt"
"sort"
"time"
)
func main() {
messages := make([]*Message, 0)
fmt.Println("Generate Messages...")
for i := 0; i < 10; i++ {
message := NewMessage()
fmt.Println(message.Id())
messages = append(messages, message)
time.Sleep(1 * time.Millisecond)
}
fmt.Println("----------------------")
fmt.Println("Sort Messages in ascending order...")
sort.SliceStable(messages, func(i, j int) bool { return messages[i].Id() < messages[j].Id()})
for i := 0; i < 10; i++ {
fmt.Println(messages[i].Id())
}
}
実行結果は次のようになり,生成順がソート後と同じであることがわかります.
Generate Messages...
01FVSHW3S537KKHBRMSA418ATB
01FVSHW3S63T5D5Q8KTT132RK3
01FVSHW3S7HW03J702MAE82MQS
01FVSHW3S99VWCKTQVG1EQB6CM
01FVSHW3SAMGGRDHPQAH2C7598
01FVSHW3SB7Q4T7GB5QP0M055N
01FVSHW3SCDQD061Q7JR9Y8VXZ
01FVSHW3SER8977QCJBYZD9HAW
01FVSHW3SF1HGC5HJ4913C9TQY
01FVSHW3SGB9GK0H2VK6GJ87PX
----------------------
Sort Messages in ascending order...
01FVSHW3S537KKHBRMSA418ATB
01FVSHW3S63T5D5Q8KTT132RK3
01FVSHW3S7HW03J702MAE82MQS
01FVSHW3S99VWCKTQVG1EQB6CM
01FVSHW3SAMGGRDHPQAH2C7598
01FVSHW3SB7Q4T7GB5QP0M055N
01FVSHW3SCDQD061Q7JR9Y8VXZ
01FVSHW3SER8977QCJBYZD9HAW
01FVSHW3SF1HGC5HJ4913C9TQY
01FVSHW3SGB9GK0H2VK6GJ87PX
ちなみに,先ほどのサンプルでは1ミリ秒のスリープを入れていましたが,無くしても同様です.
ULID を使用すれば MySQL を利用している場合にも識別子の早期生成がやりやすくなります.しかし,注意が必要なのは ULID は時刻を基に生成しているので,ユーザに露出するとリソースの生成時刻が漏洩してしまうという点です.もし生成時刻がバレたらまずいエンティティの場合は,ユーザから見える識別子とデータベースが必要とする識別子を別にするか,単にデータベース側で採番されたシーケンシャルな数字を使うかした方が望ましいかもしれません.
ちなみに, ULID に関して言及した元のツイートは自動採番された連番の識別子をユーザに露出するべきかどうかという話題に触れたものでした[11].それを踏まえても ULID は良い選択肢かもしれませんね.
-
DDDの定義は Domain-Driven Design Reference の説明が簡素でわかりやすいです. https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf ↩︎
-
IDDD本の5.2にMySQLでの事前採番について説明されています ↩︎
-
https://twitter.com/ito_yusaku/status/1490583243913330688 ↩︎
-
実は松岡さんの書かれた「ドメイン駆動設計 サンプルコード&FAQ」にもULIDかUUID使うと良いよと書かれていたのですが,完全に流していました......読んだのに...... ↩︎
Discussion