【Python3】python-ulidで一意なIDを作成する

## 環境

- Ubuntu20.04
- Python3.8.10
- python-ulid1.0.3 

## 背景

### 前提事項

オブジェクト指向においてエンティティは「同一性によって識別されるオブジェクト」とされます。つまり、エンティティは一意なIDを持つ必要があります。(一意性が担保できればIDでなくても良いですが)

### 問題意識

今までIDの採番はRDBに任せていました。具体的にはidをautoincrementにしてinsertするときに自動採番するという方式です。

この方法には以下2つの問題があります。

1. DBにinsertするまでエンティティがIDを持たない。(つまり、エンティティの定義を満たしていない状況が生まれる)
2. IDというドメイン層の関心事がインフラ層(DB)の責務になっている。


この問題を解決するには次の要件を満たす手段が必要です。

1. ドメイン層で実現する
1. オブジェクト生成時にIDがセットされる
1. 一意性が担保されている
1. 一意である(DBの主キーとして利用するため)
1. 順序が担保されている(ソートを考慮して)

これらの条件を(ほぼ)満たすのが``` ULID``` です。

## ULIDとは

https://github.com/ulid/spec

- ```Universally Unique Lexicographically Sortable Identifier```の略
- 128bit長の文字列から成る
- 1ミリ秒ごとに 1.21e+24 通りのIDを生成可能
- 辞書的にソート可能
- 特殊文字無し(URLセーフ)

ULIDは上で挙げた条件を一通り満たしています。

特別な要件が無い場合とりあえずULIDを利用してよいでしょう。

## 実演

### インストール手順

PythonでULIDを利用するのに必要な ``` python-ulid``` を下記コマンドでインストールします。

```bash
$ pip install python-ulid
```

インストールされたことを確認します。

```bash
$ pip list | grep python-ulid
python-ulid            1.0.3  
```

### 利用方法

1. ULIDクラスをインポート
1. コンストラクタでULID()メソッドを呼び出し、id属性に値をセットする。(必要に応じて型をキャストする。 今回は文字列型にしています。)


```python:Hoge.py
# ULIDクラスをインポート
from ulid import ULID

class Hoge():
    def __init__(self, name: str) -> None:
　      # ULIDクラスのオブジェクトを生成してstr型にキャスト
        self.id = str(ULID())
```

続いて実際にHogeクラスのオブジェクト生成してみます。

```python:Hoge.py

from ulid import ULID

class Hoge():
    def __init__(self, name: str) -> None:
        self.id = str(ULID())

# これより下が追記部分
hoge1 = Hoge()
print(vars(hoge1))

hoge2 = Hoge()
print(vars(hoge2))

print(hoge1.id is hoge2.id)
```

```bash
$ python3 Hoge.py

{'id': '01FS21597QRQQV7SVQSMJ3BMB3'}
{'id': '01FS21597Q7PFPTZ9G6VNW0J4M'}
False
```

IDの一意性は確認することができました。

順序が担保されているかも確認します。

```python:Hoge.py

from ulid import ULID

class Hoge():
    def __init__(self, name: str) -> None:
        self.id = str(ULID())

hoge1 = Hoge()
print(vars(hoge1))

hoge2 = Hoge()
print(vars(hoge2))

# hoge2の方が後に作成されたので、hoge2.idの方が大きくなると想定
print(hoge1.id < hoge2.id)
```

```bash
$ python3 Hoge.py

{'id': '01FS21GB08J9G59JR0GD6VQTND'}
{'id': '01FS21GB08F1PWB5N77AC0RYK8'}
False
```

hoge1のidの方が大きくなりました。

調べたところ、1ミリ秒以上の間隔をあけると確実に順序が担保されるが、それより感覚が短い場合は必ずしも順序が担保されないようです。

別の方が既に検証済みでしたので詳細は下記リンク参照。

https://note.com/setomits/n/neea91609f395

実運用で1ミリ秒以内に同一クラスのオブジェクトが生成されることは起こらないと考えて、とりあえずは無視してしまっても良いかもしれません

### 課題・懸念点

- テーブルのデータサイズが大きくなることによるパフォーマンス劣化
  - これは実際にパフォーマンスを計測してみないことには分かりません(レコード数が小さいならそれほど影響は無いと思いますが...)
 
- IDを含むエンドポイントが長くなりすぎる
  - APIの動作確認が大変になりそうです。解決方法は今のところあてがありません


### まとめ

実開発でULIDを利用するとどのような弊害があるかまだ理解できていないので実際に使ってみたいと思います。

### 参考資料

[shimojuboix](https://scrapbox.io/shimoju/ULID)
[ulid/spec](https://github.com/ulid/spec)
[ULID の順序性を確保するには](https://note.com/setomits/n/neea91609f395)
[ドメイン駆動設計入門](https://www.amazon.co.jp/%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E9%A7%86%E5%8B%95%E8%A8%AD%E8%A8%88%E5%85%A5%E9%96%80-%E3%83%9C%E3%83%88%E3%83%A0%E3%82%A2%E3%83%83%E3%83%97%E3%81%A7%E3%82%8F%E3%81%8B%E3%82%8B-%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E9%A7%86%E5%8B%95%E8%A8%AD%E8%A8%88%E3%81%AE%E5%9F%BA%E6%9C%AC-%E6%88%90%E7%80%AC-%E5%85%81%E5%AE%A3/dp/479815072X)
[ソート可能なUUID互換のulidが便利そう](https://qiita.com/kai_kou/items/b4ac2d316920e08ac75a)
[【Day 14】ID の採番問題【じゃんけんアドカレ】](https://qiita.com/os1ma/items/1f5d3dc4794fa4f510fc)