UUID から一意にFirebaseのautoIDを生成
はじめに
最近、PostgreSQL で持っていたデータを Firebase に移行するということになり移行ツールを作ることになりました。その際に PostgreSQL 側の UUID から一意に Firebase の autoID を生成するということを行ったのでメモ。
前提
元々 PostgreSQL データの主キーを UUID で持っているので、Firebase に移行したあとにデータを追加しても差異が無いよう、移行先では autoID で扱うということにしました。
PostgreSQL 側のスキーマ(厳密ではない)
# お部屋
id uuid
title string
category_id uuid
# 投稿
id uuid
body string
# ユーザ
id uuid
name string
Firestore 側のルームのスキーマ(厳密ではない)
/rooms
{id}
/posts
{id}
body string
controibutor reference(ユーザーの参照)
roomId string ("ルームのID")
title string
/users
{id}
user(自身のID)
name
新たに autoID を採番してそのまま移行しても良かったのですが、ある問題が発生します。
移行時に Firestore の reference型 を使用しているので生成済みの autoID がどんな ID なのかを探して作成しなければならないという点です。上の例で言えば posts を移行させるときにユーザーの ID どれか探してIDを当てはめる必要があります。
そこで、 UUID から 一意に autoID を生成できれば解決できるのではと考えました。
結論
結論としては以下の手順で出来ました。
- uuid のハイフンを除去
- ハッシュ化を行い一意に変換
- Base62 (64ではない)でエンコードする
- 20文字に切り詰める
具体的にはこうです。
# 元の UUID
38cbdc7d-f83f-40d1-9027-c407a26dbf4d
# 1. ハイフンを削除
38cbdc7df83f40d19027c407a26dbf4d
# 2. ソルトと UUID から ハッシュを生成
ソルト と 38cbdc7df83f40d19027c407a26dbf4d -> (ハッシュ)
# 3. Base62 でエンコード
0zW35HrXM2McSZhjLfW5NJewHAreyCiXmUetmjfGm6bG
# 4. 20文字に切り詰める
0zW35HrXM2McSZhjLfW5
ハッシュ化についてまったく分からない人は以下の記事が参考になります。
ハッシュ化とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 (i-3-i.info)
工夫点としては手順1でソルトを加えれるようにすることで、何度実行しても前回実行時の UUID から 同じ autoID が生成される仕組みにしています。
また、 Base64 ではなく Base62 を使用しています。なぜかというと autoID は記号を使わない A-Z a-z 0-9から成る20文字の ID なので Base64 に含まれている記号(+/) が含まれていては差異が生まれてしまいます。そこで記号を含まない Base62 を使うことで解決しています。
実装コード
今回は Go で移行ツールを作成したので UUID から一意の autoID を生成するコードを載せておきます。
Go のハッシュ生成ライブラリとしては bcrypt が多いと思いますが、scrypt を採用しています。bcrypt では外部からソルトを注入することが出来なかったので scrypt を使っています。
package util
import (
"github.com/google/uuid"
"github.com/jxskiss/base62" "golang.org/x/crypto/scrypt" "strings")
type AutoID struct {
salt []byte
}
func NewAutoID(salt []byte) *AutoID {
return &AutoID{
salt: salt,
}
}
func (a AutoID) Convert(uuid uuid.UUID) (string, error) {
replacedUuid := strings.Replace(uuid.String(), "-", "", -1)[:20]
convertedBytes, err := scrypt.Key([]byte(replacedUuid), a.salt, 32768, 8, 1, 32)
if err != nil {
return "", err
}
autoId := base62.EncodeToString(convertedBytes)[:20]
return autoId, nil
}
salt := []byte("これはソルトです")
autoIdClient := util.NewAutoID(salt)
uu := uuid.MustParse("38cbdc7d-f83f-40d1-9027-c407a26dbf4d")
id, err := autoIdClient.Convert(uu)
if err != nil {
return
}
fmt.Println(id) // ZgD3YDGBVgSIYjPpkKaN
Discussion