[Django]django-compositepk-model

Googleサーチコンソールを見ると、Djangoの複合キーについて調べて、以下ページにアクセスしてくれる人が多い。

そこで、GitHubで公開している複合主キー対応のパッケージについて、今更ながらREADMEの日本語版を。

https://github.com/Arisophy/django-compositepk-model

django-compositepk-modelパッケージ

複合主キーの対応のためのDjangoのModelクラスを拡張した CPkModelというクラスを提供する。Query クラスに対しても、複数カラムでのlookupsへの対応を追加したCPkQueryというサブクラスを提供する

本パッケージにより、複合主キーを使っているレガシーDBテーブルについて、サロゲートキーを追加することなくDjangoで扱えるようになる。

これはticket373に関する自分なりの解決方法です。

特徴

1. 利用方法が簡単

複合主キーを持つテーブルに対しては、基底クラスをModelクラスからCPkModelクラスに変更して、 Metaにunique_togetherを設定し、主キーを構成する各フィールド定義に primary_key=Trueを設定する。

(モデル定義の例)

from django.db import models
from cpkmodel import CPkModel

# Normal Model
#   primary_key is auto 'id'
class Company(models.Model):
    name = models.CharField(max_length=100)
    established_date = models.DateField()
    company_code = models.CharField(max_length=100)

    class Meta:
        db_table = 'Company'

# Child Model (CpkModel)
#   composite primary key: company_id, country_code
class CompanyBranch(CPkModel):
    company = models.ForeignKey(
        Company,
        primary_key=True,       # for CompositePK
        on_delete=models.CASCADE,
    )
    country_code = models.CharField(
        max_length=100,
        primary_key=True,       # for CompositePK
    )
    name = models.CharField(max_length=100)
    established_date = models.DateField()

    class Meta:
        managed = False  # for CompositePK *1
        db_table = 'CompanyBranch'
        unique_together = (('company', 'country_code'),)  # for CompositePK

それだけで、それ以外の追加定義、仮想フィールドの追加は必要ない。

*1:  primary_key=Trueが複数カラムに設定されていると、マイグレーションが失敗するので、 managed = Falseとしている。レガシーDBのテーブルは既に存在するか、手でCREATEされると思うので。マイグレーションが使いたければ、migrationする時にprimary_key=Trueとmanaged=Falseをコメントアウトすること。

2. Admin画面が使える

CPkModelを継承したモデルも、DjangoのAdmin画面で使える。 複合キーの値は、 カンマ区切り表示される。 Change(Update)とDeleteは問題なく動く。 Add(Create) に関しては、 CreateViewがキー項目のそれぞれに対してユニークチェックをするので、最初の子レコードは登録できるけど、追加の子レコードが登録できなくなるという問題がある。 これは、CreateViewの問題なので、QuerySetやModelのメソッドを使うプログラムは問題ない。

3. 複数カラムでのlookupsに対応

複合主キーに対応するため、CPkQueryで複数カラムを指定したlookup(以下)に対応している。

obj = CompanyBranch.objects.get(pk=(1,'JP'))
qs = CompanyBranch.objects.filter(pk='1,JP')
qs = CompanyBranch.objects.filter(**{'company,country_code':(1,'JP')})
qs = CompanyBranch.objects.filter(**{'company_id,country_code':'1,JP'})

カンマの入ったLHS(左辺)は、主キーだけでなく、他のカラムの組み合わせもOK。

qs = CompanyBranch.objects.filter(
    **{'country_code,name':'JP,Google'})

IN句に対しても、複数カラムが使える。PostgreSQLは問題ないが、SQLite3についてはIN句に対応されていない(※Djangoの問題)のでエラーになる。

qs = CompanyBranch.objects.filter(
    pk__in=[(1,'JP'),(1,'US'),(2,'JP'),])
qs = CompanyBranch.objects.filter(
    **{'country_code,name__in':[('JP','HONDA'),('CN','SONY'),]})

4. bulk_updateが使える(v1.0.2)

bulk_updateメソッドも、PostgreSQLについて利用可能。SQLite3はサポートされていないので、不可。

    Album.objects.bulk_update(
        albums, ['num_stars',])

制約

1. マイグレーション(table作成)

primary_key=Trueが複数カラムに設定されていると、マイグレーションが失敗するので、 managed = Falseとしている。 レガシーなテーブルは既に存在するか、手でCREATEされるので。マイグレーションが使いたければ、migrationする時にはprimary_key=Trueとmanaged=Falseをコメントアウトすること。

2. Admin画面でのCREATE(CreateViewの問題)

CreateViewがキー項目のそれぞれに対してユニークチェックをするので、レコードが一部しか登録できなくなる。 これは、CreateViewの問題なので、QuerySetやModelのメソッドを使うプログラムには問題ない。

3. 外部キー

孫テーブルへのリレーションをサポートするにはForeignKeyからCPkForeignKeyというクラスを作る必要があるが、まだ未対応。

4. SQLiteでIN句が使えない

Djangoのモデルは 、SQLiteのIN句に対応していない。

5. レコード登録は、saveよりCREATEが良い

INSERTについては、CPKModelのsaveを使うより、CPKQuerySetのcreateメソッドを使う方がいい。何故なら、Modelのsaveでは、キー値が設定されている場合、最初にUPDATEを行うので(その次にINSERTを実行)。 オプションでforce_insert=Trueを指定することで、避けることは可能ではあるが。

CompanyBranch.objects.create(**params)

 または 
 
obj = CompanyBranch(**params)
obj.save(force_insert=True)

インストール方法

pip install django-compositepk-model

リンク

https://code.djangoproject.com/ticket/373
https://code.djangoproject.com/wiki/MultipleColumnPrimaryKeys
https://gijutsu.com/2021/01/19/django-composite-primary-key/
https://gijutsu.com/2021/03/16/django-ticket373/