この記事は夏のブログリレー17日目の記事です。
22Bのいくら・はむです。GoでMongoDBのドライバー(mongo-driver
)でUUIDを使うときのことを書きます。
MongoDBとは
NoSQLのデータベースの一種です。「BSON」と呼ばれるJSONのような形式の「ドキュメント」を保持します。RDBでいうテーブルは「コレクション」、レコードは「ドキュメント」と呼ばれます。カラムにあたるものは「フィールド」ですが、RDBとは異なり制約がゆるく、自由度が高いのが特徴です。
UUIDとは
Universally Unique Identifier の略です。128ビットの数値で、16進数に直して32個の数字とアルファベットで表されることが多いです。数が多く衝突が起きにくいので、かぶると困るIDなどをアプリケーションで生成するのによく使われます。traPの部内SNSであるtraQでも、全てのユーザーやチャンネル、メッセージにこのUUIDが割り振られています。
https://ja.wikipedia.org/wiki/UUID
Goには様々なUUIDを扱うためのライブラリがあります。例えばgithub.com/google/uuid
があります。このようなライブラリでは、UUID
型を下のように定義しています。
type UUID [16]byte
GoのMongoDBドライバー
構造体とomitempty
GoのMongoDBドライバー(mongo-driver
)では、データベースからの検索や挿入に構造体を使えます。
type User struct {
Id int `bson:"id,omitempty"`
Name string `bson:"name,omitempty"`
}
// idが100で名前がikura-hamuのユーザーを追加
_, err := br.db.Collection("users").InsertOne(ctx, User{
Id: 100,
Name: "ikura-hamu",
})
var user User
// idが200のユーザーを1人取得
err = br.db.Collection("users").FindOne(ctx, User{
Id: 200,
}).Decode(&user)
構造体の定義に書かれたomitempty
は、MongoDBでBSONとして扱うとき、値がゼロ値であればフィールドが省略されるという意味です。ユーザー取得の方ではId
しか指定してないので、Name
のフィールドは無視され、BSONは下のようになります。
{
"id":200
}
構造体をmongo-driver
でBSONにしたときの結果は、bson.Marshal()
を使って調べることができ、bson.Raw()
でいい感じに出力できます。
UUIDは?
UUID型をBSONにするとこうなります。
type User struct {
Id uuid.UUID `bson:"id,omitempty"`
Name string `bson:"name,omitempty"`
}
u := User{
Id: uuid.New(),
Name: "ikura-hamu",
}
b, _ := bson.Marshal(u)
fmt.Println(bson.Raw(b))
{
"id": {
"$binary":{
"base64":"Wn5T1LzrSziAVlaR2Ix1MA==",
"subType":"00"
}
},
"name": "ikura-hamu"
}
$binary
というものが出てきました。これはUUIDが[16]byte
で表されているため、バイナリとして認識されているのです。
https://www.mongodb.com/docs/v7.0/reference/bson-types/#binary-data
ではId
を指定せずにゼロ値にするとどうなるでしょうか。
type User struct {
Id uuid.UUID `bson:"id,omitempty"`
Name string `bson:"name,omitempty"`
}
u := User{
Name: "ikura-hamu",
}
b, _ := bson.Marshal(u)
fmt.Println(bson.Raw(b))
{
"id": {
"$binary":{
"base64":"AAAAAAAAAAAAAAAAAAAAAA==",
"subType":"00"
}
},
"name": "ikura-hamu"
}
"AAAAAAAAAAAAAAAAAAAAAA=="
という謎の値になってしまいました。
これはUUIDのゼロ値が00000000-0000-0000-0000-000000000000
だからです。これがomitempty
ではゼロ値と判断されずBSONのフィールドとして残ってしまいます。
Go Playground で試せます。
https://go.dev/play/p/6vcynRL1npM
これで検索をすると?
UUIDが含まれる構造体で、UUIDに値を入れずにMongoDBの検索をしようとするとうまくいきません。
"$binary":{
"base64":"AAAAAAAAAAAAAAAAAAAAAA==",
"subType":"00"
}
これに該当する値を検索しようとするからです。
これを避けるために、検索用の構造体を新たに定義するか、bson.M
などを使って構造体以外を使いましょう。
2024/4/17 追記
ポインタにすればいいのか
おわり
最近初めてMongoDBを触ってこのomitempty
の罠にかなり時間を取られました。大変。これの他に、bool
でfalse
がゼロ値なのを忘れてomitempty
をつけて、こちらは消えてほしくないフィールドが意図せず消えてしまいました。気を付けたいです。
MongoDBはちゃんと使い分けをしてうまく使いこなせばRDBより使いやすくなりそうなので、いろいろ試してみたいです。
明日は @ramdosくんの記事です。