環境
- 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 は
- (ほぼ)生成時刻によりソート可能
- UUID v.4 は完全ランダムなので不可
- 生成マシンのMACアドレスを晒さない
- UUID v.1 はMACアドレスを晒してしまう
などの特徴があります。オリジナルな実装は JavaScript ですが主要な言語に移植されています。Python でも複数の実装がありますが、ここでは GitHub でのスター数が最も多い(2017/12/9時点で47)@ahawker さんの実装を使って Django のモデルの ID を ULID にしてみます。
Django で ULID
はじめに ulid パッケージをインストールします。
$ pip install ulid-py
生成してみます。
>>> import ulid
>>> ulid.new()
<ULID('01C0WNCF4HZBR7ZRPWTWD6C080')>
簡単ですね。これを Django のモデルに組み込みましょう。
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
を作ってみましょう。
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文字の固定長文字列にマップされます。
最終的にはこうなります。
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 を使うことで、シリアルなキーの問題点が緩和されました。
-
子オブジェクトが親オブジェクトへの参照を持っているとき、子オブジェクトを insert するには 親オブジェクトの primary key を指定する必要があるので、
AUTOINCREMENT
などで DB に採番させている場合、親オブジェクトを insert →親オブジェクトの ID を DB から取得 → 子オブジェクトの foreign key に、取得した親オブジェクトを指定、という面倒な手順を踏む必要があります。 ↩ -
ほかにも色々な種類があります。kawasima さんの [ID生成大全] (https://qiita.com/kawasima/items/6b0f47a60c9cb5ffb5c4) という記事がとても参考になります。 ↩
-
Django 本体に固定長文字列を入れるリクエストが以前あったようですがリジェクトされたみたいですね。残念。 ↩
-
Django の ドキュメンテーション: Writing custom model fields ↩