Django: Primary key を ULID にする

# 環境

- Django 1.11

# はじめに
Django は各モデルに対して `id` という名前の主キーフィールドを自動的に設定します。`id` の DB内 での列定義には `SERIAL` とか `AUTOINCREMENT` が付き、採番はDB側で行われることになります。

シリアルな主キーは分かりやすく、生成した順にソートできるなどの利点がありますが、次のような欠点もあります:

- 分散環境で生成すると衝突する
- `INSERT` するまで確定しない
    - 採番が DB によって行われるため。
    - 再帰的な関係を持つモデルで、親オブジェクトと子オブジェクトを同時に bulk insert できない[^1]。
- 容易に推測可能
    - セキュリティ上問題になるケースがある。

ID にランダムな要素を混ぜるとこれらの欠点を緩和することができます。ランダムな要素をもつ ID としては UUID v.1 と v.4 が有名ですが、ここでは ULID というものを使います[^2]。

UUID も ULID も共に128bit の ID ですが、ULID は

- （ほぼ）生成時刻によりソート可能
    - :x:UUID v.4 は完全ランダムなので不可
- 生成マシンのMACアドレスを晒さない
    - :x:UUID v.1 はMACアドレスを晒してしまう

などの特徴があります。[オリジナルな実装](https://github.com/ulid)は JavaScript ですが[主要な言語に移植されています](https://github.com/ulid/spec)。Python でも複数の実装がありますが、ここでは GitHub でのスター数が最も多い（2017/12/9時点で47）[@ahawker さんの実装](https://github.com/ahawker/ulid)を使って Django のモデルの ID を ULID にしてみます。

# Django で ULID

はじめに ulid パッケージをインストールします。

```
    $ pip install ulid-py
```

生成してみます。

```py3
>>> import ulid
>>> ulid.new()
<ULID('01C0WNCF4HZBR7ZRPWTWD6C080')>
```

簡単ですね。これを Django のモデルに組み込みましょう。

```py3:models.py
import ulid

from django.db import models

# Create your models here.

class Article(models.Model):

    id = models.CharField(
        default=ulid.new,
        max_length=26,
        primary_key=True,
        editable=False
    )
    content = models.TextField()
```

これだけで最低限のことはできます。ポイントは

```
        default=ulid.new,
```
です。`ulid.new()` は先ほど見たように ULID を生成して返す関数ですが、default 引数に callable を指定することで Django がオブジェクト生成のたびにそれを呼び出し、返り値をそのフィールドの値にしてくれます。

これでも動きますが、パフォーマンス面で改善の余地があります。Django の CharField は DB の可変長文字列（`varchar`）にマップされますが、ULID の文字列表現は26文字固定なので、パフォーマンスが優れている固定長文字列（`char`）にしたいところです（ただし SQLite3 は `char` に対応しておらず、`char` を `varchar` として扱うので効果はありません）。Django 本体は固定長文字列に対応するモデルフィールドを持っていません[^3]が、これは簡単に自作することができます[^4]。26文字固定の `ULIDField` を作ってみましょう。

```py3
class ULIDField(models.CharField):

    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 26
        super(ULIDField, self).__init__(*args, **kwargs)

    def db_type(self, connection):
        return 'char(26)'
```

ポイントは

```
    def db_type(self, connection):
        return 'char(26)'
```
で、すでにお気づきかもしれませんが `db_type()` は DB でのデータ型を文字列として返すメソッドです。これが `char(26)` を返却するようにすればそのフィールドは26文字の固定長文字列にマップされます。

最終的にはこうなります。

```py3:models.py
import ulid

from django.db import models

# Create your models here.

class ULIDField(models.CharField):

    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 26
        super(ULIDField, self).__init__(*args, **kwargs)

    def db_type(self, connection):
        return 'char(26)'


class Article(models.Model):

    id = ULIDField(
        default=ulid.new,
        primary_key=True,
        editable=False
    )
    content = models.TextField()
```

# まとめ

- 分散環境で生成されたIDどうしが衝突する確率は0ではないが極めて低い
- INSERT 前であっても `ulid.new()` を呼び出せばアプリケーション側で主キー値を知ることができる
- ランダムな要素が混ざっているので推測されにくい

ULID を使うことで、シリアルなキーの問題点が緩和されました:ok_woman_tone1:。

[^1]: 子オブジェクトが親オブジェクトへの参照を持っているとき、子オブジェクトを insert するには 親オブジェクトの primary key を指定する必要があるので、`AUTOINCREMENT` などで DB に採番させている場合、親オブジェクトを insert →親オブジェクトの ID を DB から取得 → 子オブジェクトの foreign key に、取得した親オブジェクトを指定、という面倒な手順を踏む必要があります。

[^2]: ほかにも色々な種類があります。kawasima さんの [ID生成大全] (https://qiita.com/kawasima/items/6b0f47a60c9cb5ffb5c4) という記事がとても参考になります。

[^3]: [Django 本体に固定長文字列を入れるリクエスト](https://code.djangoproject.com/ticket/9349)が以前あったようですがリジェクトされたみたいですね。残念。

[^4]: Django の ドキュメンテーション: [Writing custom model fields](https://docs.djangoproject.com/en/1.11/howto/custom-model-fields/)