この投稿は 「Calendar for Django | Advent Calendar 2022 - Qiita」 2日目の記事です。
最近こんなの見つけたよという緩い内容になっているので、「こんなのもあるんだな」という軽い気持ちで読んでいただければと思います。そしてもし、「現場で使ってるよ」という方がいれば教えていただければありがたいです。
課題
まず大前提ですが、Django モデルの仕様として、それぞれのモデルごとに単一の主キーを持たせる必要があり(「primary=True」オプションを持つフィールドをモデルに明示的に含めるか、さもなくば「id」という名前のフィールドが自動的にモデルに追加され、それに対応したカラムがテーブルに作成される)、複合主キー(composite primary key)はサポートされていません。 *1
そのため、例えば、単一の主キーを持たない(が複合主キーを持っている)既存のテーブルを Django モデルで扱うことは困難です。
ところで Django で複合主キーっぽいことをしたければ通常は、「id」という名前のサロゲートキーを主キーとして別に用意しつつ、複合ユニーク制約を利用しますよね。具体的には、Django 2.2 から追加された UniqueConstraint を利用して次のように実装します。
models.py
from django.db import models class Employee(models.Model): """従業員モデル""" class Meta: db_table = 'employee' verbose_name = verbose_name_plural = '従業員' constraints = [ models.UniqueConstraint(fields=['branch_code', 'employee_code'], name='unique_employee') ] branch_code = models.CharField('支店コード', max_length=3) employee_code = models.CharField('従業員コード', max_length=5) name = models.CharField('従業員名', max_length=255) def __str__(self): return f'{self.branch_code}-{self.employee_code}'
この従業員テーブルの仕様としては、支店ごとに従業員コードが振られていて、従業員コードはレコード全体としてはユニークになっていない(支店コードと従業員コードを合わせるとユニークになる)という想定です。
ちなみに複合ユニーク制約を実現するには unique_together を使うことも可能ですが、将来的に非推奨になる可能性があり、UniqueConstraint の方が多機能なので、UniqueConstraint の利用が推奨されます。
解決策
最近見つけたのですが、(次期リリースではありますが)「Viewflow」というライブラリの「CompositeKey」を使えば、既存テーブルに主キーがないテーブルを Django モデルで扱うことが可能になります。
使い方は次の通りです。
pip install django-viewflow --pre # or pip install django-viewflow==2.0.0a2
models.py
from django.db import models from viewflow.fields import CompositeKey class Employee(models.Model): """従業員モデル""" class Meta: db_table = 'employee' managed = False verbose_name = verbose_name_plural = '従業員' id = CompositeKey(columns=['branch_code', 'employee_code']) branch_code = models.CharField('支店コード', max_length=3) employee_code = models.CharField('従業員コード', max_length=5) name = models.CharField('従業員名', max_length=255) def __str__(self): return f'{self.branch_code}-{self.employee_code}'
これで、CompositeKey の columns で指定した複数のフィールドの値を「{'branch_code': '001', 'employee_code': '00001'}」のような JSON 文字列で保持する「id」という名前の 仮想フィールド を持つことができます。
「managed = False」*2 を指定しないとマイグレーションで「id」カラムが作成されてしまうのですが、CompositeKey が威力を発揮するのは「managed = False」を指定してモデルをマイグレーションの対象外にした場合で、主キーを持たない既存のテーブルに影響を及ぼさずに Django モデルを用意することができます。事例としては、次のように複合主キーを持った既存テーブルから Django ORM を使ってレコードを抽出(読み取り専用)したいというニーズに応えることができます。
Django 管理サイトでも動作するというのも高評価です。
*1:Django で複合主キーをサポートするかどうかは、17年前から議論が続いています… #373 (Add support for multiple-column primary keys) – Django
*2:https://docs.djangoproject.com/ja/3.2/ref/models/options/#managed