31
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Django: Primary key を ULID にする

Last updated at Posted at 2017-12-09

環境

  • 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アドレスを晒してしまう

などの特徴があります。オリジナルな実装は 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 のモデルに組み込みましょう。

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 に対応しておらず、charvarchar として扱うので効果はありません)。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文字の固定長文字列にマップされます。

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

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 本体に固定長文字列を入れるリクエストが以前あったようですがリジェクトされたみたいですね。残念。

  4. Django の ドキュメンテーション: Writing custom model fields

31
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?