akiyoko blog

akiyoko の IT技術系ブログです

既存テーブルに単一の主キーがない場合に Django モデルを使いたい場合の解決案(Django で複合主キーを使う方法)

この投稿は 「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