[Django]django-compositepk-model

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

https://gijutsu.com/2021/01/19/django-composite-primary-key/

そこで、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/

[Django]ソース構成について

Djangoへの移行作業で、VisualStudioのサンプルコードを参考に開発を始めたので、アプリケーションの主なソース構成はいわゆるMVCパターンで以下となっていた。

ソースファイル備考
V:ビューapp/templates/
C:コントローラーapp/views.py
M:モデルapp/models.py
app/managers.py
DBデータの定義
Managerの拡張クラス。

移行するシステムは、それなりに大きくて、上記構成では各ファイルが大きくなってきて、分かりにくくなってきた。そこで、構成を見直した。

新しい構成としては、以下のようにしてみた。

階層ソースファイル備考
V:ビューapp/templates/
C:コントローラーapp/views.pyクラスビューを定義
C:コントローラーapp/contorollers.pyリクエストに応じて、プロシージャやサービスを呼び出す。
BP:プロシージャapp/procedures.pyサービスをシーケンス呼び出しする手順
BS:サービスapp/services.pyここがいわゆるビジネスロジック。ここが大きくなりそうなら、ディレクトリにしてファイル分割する。
DP:データプロバイダーapp/providers.pyデータへのアクセスを提供。SpringでいうRepositoryの位置づけ。
Model,Managerにはここからアクセスする。
M:モデルapp/models.pyDBデータの定義

Managerの拡張クラスを使わなくした理由は、個人的にDjangoのModelとMangerの関係が、Mangerに処理を定義していくと、Modelとの相互参照の問題も出てくるという作りがどうも好きになれないので。相互参照を避けるための方法は提供されているが・・・。

上記の構成にするため、簡単なフレームワークを作成した。まだ直さないといけない部分はあるが、とりあえずは、この方針で進めてみる。

[Django]チケット#373について

チケット#373(Add support for multiple-column primary keys)については、Djangoの以下Wikiに詳しく書いてある。

https://code.djangoproject.com/wiki/MultipleColumnPrimaryKeys

これに関する自分の意見と、django-compositepk-modelではどのように実装したかを述べる。

0. django-compositepk-modelについて

複合主キーの対応については、Djangoのモデルに手を入れるのが良い実装になると思うが、対応されるのかもわからない状況で待てない。Djangoから分離して独自ブランチで進めるという手もあるが、先々のメンテナンスを考えるとそれも良くない。

ということで、拡張クラスとしてパッケージにした。直接手が入れられないため、1行を変更するために、オーバーライドして元ソースを丸ごとコピーして直すという間抜けな実装もあるが、基本的な部分は上手くフックできていると思う。

GitHub上にテスト用のデータ設定済みのSQLiteのデータも含まれていて、Admin画面も実行できるので、興味のある人は動かしてみて下さい。

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

1. WikiのMajor Issuesについて

https://code.djangoproject.com/wiki/MultipleColumnPrimaryKeys#MajorIssues

1.1 _meta.pkについて

1. A number of APIs use “obj._meta.pk” to access the primary key, on the assumption it is a single field (for example, to do “pk=whatever” lookups). A composite PK implementation would need to emulate this in some way to avoid breaking everything.

Django Community Wiki

1) multi field対応

CPKModelでは、_meta.pkに設定するものを、複数フィールドをまとめるCompositePkというFieldのサブクラスに変更した。

(compositekey.py)

class CompositeKey(Field):
    def __init__(self, keys, primary=False):
        names = tuple((f.name for f in keys))
        join_name = CPK_SEP.join(names)
        db_columns = tuple((f.db_column if f.db_column else f.name for f in keys))
        db_join_column = "(" + ",".join(db_columns) + ")" """! カンマ区切りに !"""
        super().__init__(
                name=join_name, 
                primary_key=primary,
                unique=True,
        )
        self.keys = keys
        self.attname = join_name
        self.column = join_name
        self.names = names
        self.model = keys[0].model

    def get_col(self, alias, output_field=None):
        return CompositeCol(alias, self, output_field)

(cpkmodel.py)

class CompositePk(CompositeKey):
    def __init__(self, keys):
        super().__init__(keys, primary=True)
:

class CPkModelBase(ModelBase):
    """ Metaclass for CompositePkModel."""
    def __new__(cls, name, bases, attrs, **kwargs):
            :
            super_new = super().__new__(cls, name, tuple(modelbases), attrs, **kwargs)
            meta = super_new._meta
            pkeys = tuple(f for f in meta.local_concrete_fields if f.primary_key)
            # change attributes
            if len(pkeys) > 1:
                super_new.has_compositepk = True
                meta.pk = CompositePk(pkeys)  """! _meta.pkにCompositePkを設定 !"""
                setattr(super_new, "pk", CPkModelMixin.cpk)
                setattr(super_new, "_get_pk_val", CPkModelMixin._get_cpk_val)
                setattr(super_new, "_set_pk_val", CPkModelMixin._set_cpk_val)
                setattr(super_new, meta.pk.attname, None)
                setattr(super_new, "_check_single_primary_key", CPkModelMixin._no_check)
                setattr(super_new, "delete", CPkModelMixin.delete)
            else:
                super_new.has_compositepk = False
            setattr(super_new, "get_pk_lookups", CPkModelMixin.get_pk_lookups)
            meta.base_manager._queryset_class = CPkQuerySet
            meta.default_manager._queryset_class = CPkQuerySet           
            super_new.pkeys = pkeys
            super_new.pkvals = CPkModelMixin.pkvals
            super_new._meta = meta
            return super_new

class CPkModel(CPkModelMixin, Model, metaclass=CPkModelBase):
    pass

従来のModelとCpkModelの設定値の違いは以下のようになる。※テスト画面から「CheckKeys」からも確認できる。

#Musician(Model)Album(CPkModel)
obj.pk11,1
_meta.pktest.Musician.idtest.Album.artist,album_no
_meta.pk ClassAutoFieldCompositePk
_meta.pk.keys(<django.db.models.fields.related.ForeignKey: artist>, <django.db.models.fields.IntegerField: album_no>)
_meta.pk.nameidartist,album_no
_meta.pk.attnameidartist,album_no
_meta.pk.columnidartist,album_no
_meta.pk.names(‘artist’, ‘album_no’)
_meta.pk.modelMusician objectAlbum object
_meta.pk.ColCol(Musician, test.Musician.id)CompositeCol(Album, test.Album.artist,album_no)

2) pk=”whatever” lookupsの対応

[pk=”whatever” lookups]のほとんどは、最終的には、Query.add_qで処理される。そこで、CompositePkが作った特殊なpkと、それに対応する複数の値を持つ”whatever”をここでうまく処理すればいい。

方針としては、以下で対応した。

2-1) 複合主キーの条件をadd_qの中で複数条件に変更

例えば、”pk=obj.pk”という条件は以下のように変更する。

Albumの場合、_meta.pk.attnameから左辺は、”artist,album_no”となる。右辺のobj.pkは”1,1″のような形になる。両辺をカンマで分解して二つのQ条件にする。

Q(artisit='1') & Q(album_no='1')

このクエリは、最終的に、以下のようなSQL条件文になる。

"Album"."artist_id" = 1 AND "Album"."album_no" = 1 

実装している部分は、以下である。

(cpkquery.py)

class CPkQueryMixin():
   :
    ###########################
    # override
    ###########################
     :
    def add_q(self, q_object):
       :
            def make_q(keys, vals):
                q = Q()
                for key, val in zip(keys, vals):
                    q.children.append((key, val))
                return q

            assert isinstance(obj, (Q, tuple))
            if isinstance(obj, Q):
                :
            else:
                # When obj is tuple,
                #  obj[0] is lhs(lookup expression)
                #       pk and multi column with lookup 'in' is nothing to do in this, it will change in 'names_to_path'. 
                #  obj[1] is rhs(values)
                #       valeus are separated in this method.
                names = obj[0].split(LOOKUP_SEP)
                if ('pk' in names and self.model.has_compositepk) or CPK_SEP in obj[0]:
                    # When composite-pk or multi-column
                    if len(names) == 1:
                        # change one Q to multi Q
                        keys = separate_key(self, obj[0]) """! キーを分ける !"""
                        vals = separate_value(keys, obj[1]) """! 値を分ける !"""
                        if len(keys) == len(vals):
                            return make_q(keys, vals) """! make multi conditions !"""
                        else:
                            raise ProgrammingError("Parameter unmatch : key={} val={}".format(keys, vals))
                    else:
                        # check the last name
                        last = names[-1]
                        if last == 'in':
                            :
                        elif last == 'pk' or CPK_SEP in last:
                            # change one Q to multi Q
                            #  example: ('relmodel__id1,id2', (valule1,value2))
                            #             |
                            #             V
                            #           ('relmodel__id1', valule1)
                            #           ('relmodel__id2', valule2)
                            before_path = LOOKUP_SEP.join(names[0:-1])
                            cols = separate_key(self, last)
                            keys = [before_path +  LOOKUP_SEP + col for col in cols] """! キーを分ける !"""
                            vals = separate_value(cols, obj[1])  """! 値を分ける !"""
                            return make_q(keys, vals)    """! make multi conditions !"""
                        else:
                            # another lookup is not supported.
                            raise NotSupportedError("Not supported multi-column with '{}' : {}".format(last,obj[0]))
                return obj

        new_q = transform_q(q_object)
        super().add_q(new_q)
2-2) 複数カラムIN句の対応

IN句については、pkはadd_qで加工はしないで、そのままCompositePkのままにする。値については、カンマ区切りのものは分解してタプルにする。

例えば、”pk__in=[‘1,JP’,’1,US’,’2,JP’,]”という条件は以下のように変更される。

pk__in=[(1,'JP'),(1,'US'),(2,'JP')])

DBカラム名を作成するColクラスは、CompositePkにおいては、CompositeColサブクラスにて処理されて、以下表現を返す。

("CompanyBranch"."company_id", "CompanyBranch"."country_code")

最終的に、以下のようなSQL条件文になる。

("CompanyBranch"."company_id", "CompanyBranch"."country_code") IN ((1,"JP"), (1,"US"), (2,"JP"))

なお、主キーだけでなく、任意のカラムでもカンマ区切りで複数指定ができるようにした。

(compositekey.py)

class CompositeCol(Col):
    def __init__(self, alias, target, output_field=None):
        super().__init__(alias, target, output_field)
        self.children = [Col(alias, key, output_field) for key in target.keys]

    def as_sql(self, compiler, connection):
        sqls = []
        for child in self.children:
            sql, _ = child.as_sql(compiler, connection)
            sqls.append(sql)
        return "(%s)" % ",".join(sqls), []

class CompositeKey(Field):
    :
    def get_col(self, alias, output_field=None):
        return CompositeCol(alias, self, output_field)   """! CompositeColを返す !"""

(cpkquery.py)

class CPkQueryMixin():
    :
    ###########################
    # override
    ###########################
    :
    def names_to_path(self, names, opts, allow_many=True, fail_on_missing=False):
        meta = self.get_meta()
        first_name = names[0]
        # name[0] is Multi-Column ?
        if (first_name == 'pk' and self.model.has_compositepk) or CPK_SEP in first_name:
            # get CompisteKey
            ckey = meta.pk
            if first_name != 'pk' and first_name != ckey.name:
                # IF Not PK, make another CompositeKey
                cols = [meta.get_field(col) for col in first_name.split(CPK_SEP)]
                ckey = CompositeKey(cols)    """! 複数カラムをCompositeKeyでまとめる !"""
            lookups = names[1:] if len(names) > 1 else []
            return [], ckey, (ckey,), lookups
        else:
            return super().names_to_path(names, opts, allow_many, fail_on_missing)

1.2 A number of things use (content_type_id, object_pk) tuples

2. A number of things use (content_type_id, object_pk) tuples to refer to some object — look at the comment framework, or the admin log API. Again, a composite PK system would need to somehow not break this.

Django Community Wiki

“object_pk” が CompositePkに変わっても、問題ないと思う。実際、CompositePkを使うCPkModelをAdmin画面で使っても問題は特に起きていない。

1.3 Admin URL

3. Admin URLs; they’re of the form “/app_label/module_name/pk/”; there would need to be a way to map URLs to objects with a set of columns for the primary key.

Django Community Wiki

CompositePkは、カンマ区切りの文字を返す。URLはエスケープされて問題なく使えている。

Django Admin for Composite PrimaryKey
Django Admin for Composite PrimaryKey

1.4 結論

_meta.pkを、CompositePkという複数フィールドを管理するサブクラスに置き換えるという考え方で、Major Issuesを簡単に解決できた。

2. 今後の課題

django-compositepk-modelの課題、制約は以下。

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

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

CPkForegnKey以外は、Djangoのソースを直接いじれば、簡単に解決できる問題だと思う。

現状のままでも、自分のプロジェクトでは、今のところ問題なく使えている。また、今は、リリースに向けてパフォーマンス見直しおしており、ORMのリレーションは使わずに、逆にRawクエリに直しているところで、CPkForegnKeyは無くても困っていない。

いつか余裕ができたら、CPkForegnKeyの追加、あるいは、Djangoそのものに手を入れることも考えてみたい。

[Django]DBアクセスを減らす

最近は、外でSurfaceで開発しているので、容量もそれほどなくあまり色々入れたくないというのと、外に持ち運ぶ端末内にお客さんの個人情報を入れたくないということで、開発環境のWEBサーバ―はローカルPC内だが、DBはクラウドサーバー上にあるという変な形でやっている。

この環境だとDBアクセスのオーバーヘッドが顕著にあらわれる。ORMを何も考えないで使っていると、処理によっては、結構、待たされる。ということで、そろそろリリースに向けた見直し中。

1.モデルの使い方

Djangoのモデルを効率よく使う方法は、以下ページに書かれている。

https://docs.djangoproject.com/ja/3.1/topics/db/optimization/

複雑な参照系は、Rawクエリで一発で取ってくるようにしているが、微妙なところはモデルをそのまま使うようにしている。その部分で、余分なクエリが結構発生しているので直す。

1.1 外部キー

1)必要のない参照は消す

プログラム内で外部キーを参照するときに、モデル上のリレーションモデルからキーを参照すると、リレーションモデルの取得が発生するので、書き方を見直す。

https://docs.djangoproject.com/ja/3.1/topics/db/optimization/#use-foreign-key-values-directly

albumモデル上でartistを参照している場合に、albumからartisitのidを取得する場合には以下のようにする。

(正)album.artist_id
(誤)album.artist.id  ※artisitをDBから取得する

この延長で、リレーションのそのまたリレーションからモデルを参照するのは、中間のモデルが必要ない場合は直す。以下では、companyを参照しているが、その前にartistもDBから取得されてしまうので、クエリで取得するように直す。

(例)company = album.artist.company  ※artisitもDBから取得してしまう

2)必要な参照は同時に取る

select_relatedを使って、参照も同時に取る。

https://docs.djangoproject.com/ja/3.1/ref/models/querysets/#django.db.models.query.QuerySet.select_related

3)必要な参照はまとめて取る

prefech_relatedを使って、参照を一気に取る。select_relatedとの違いは、ベースのデータを取得時にselect_relatedは結合で同時に取るというのに対して、prefech_relatedは参照テーブルのデータを事前に一気に取って、メモリ上で結合するという感じ。

https://docs.djangoproject.com/ja/3.1/ref/models/querysets/#django.db.models.query.QuerySet.prefetch_related

1.2 キャッシュの利用

1)メソッドのキャッシュ化

モデルから外部データを取得するメソッドに、cached_propertyを付けることで、モデルインスタンス上でキャッシュされるようになる。

https://docs.djangoproject.com/ja/3.1/ref/utils/#django.utils.functional.cached_property

2)マスタのキャッシュ

DBアクセスを減らすと基本と言えば、マスタ系をキャッシュするということで、Djangoのキャッシュ機能については以下。

https://docs.djangoproject.com/ja/3.1/topics/cache/

ページ単位のキャッシュ等が出てくるが、マスタDB等のキャッシュとしては、以下の「低レベルキャッシュAPI」を使うことが考えられる。

https://docs.djangoproject.com/ja/3.1/topics/cache/#the-low-level-cache-api

バックエンドとして、色々設定できるようだが、簡単なマスタのキャッシュということで、選択肢はローカルメモリしかありえない。

キャッシュ機能を使わなくても、マスタ管理のクラスを作ってグルーバルにインスタンス定義すれば、1)のcashed_propertyで実装できるか。

1.3 一括更新

一括更新(bulk_update)については以下。

https://docs.djangoproject.com/ja/3.1/topics/db/optimization/#update-in-bulk

複合主キーのレガシーDBを使っており、以下パッケージで対応した。

django-compositepk-model

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

1.4 登録

複合主キーの登録については、注意が必要。無指定でModel.saveを使うと、最初にUPDATEをしに行くので、force_insert=Trueを指定するか、QuerySet.createを使う。

https://github.com/Arisophy/django-compositepk-model/blob/main/README.md#5-create-is-better-than-save

2.独自クエリ

一覧画面系は、最初からさっさと独自クエリで実装した。

https://docs.djangoproject.com/ja/3.1/topics/db/sql/#executing-custom-sql-directly

一覧を取得する処理は、カスタムmanager上に実装して、namedtupleで渡すほうが扱いやすい。更新にも使う場合は、modelにつめて返すようにした。

[Django]複合主キー

0.Djangoは複合主キーをサポートしている?

https://docs.djangoproject.com/ja/3.1/faq/models/#do-django-models-support-multiple-column-primary-keys

https://code.djangoproject.com/wiki/MultipleColumnPrimaryKeys

Djangoでは複合主キーのサポートは公式にはされていない。

「primary_key=True」が指定されているフィールドが無い場合は、自動で「id」が主キーとして追加されて動作するようなので、従来システムのDBを使っている場合、モデルを使った更新、削除が、このままでは動作しない。テーブルにidフィールドを追加するか、独自のクエリで処理する必要がある。

1.解決方法

案1:サロゲートキーを追加する

サロゲートキー(“id”)をDBに追加して、複合キーには一意制約(unique_together)を設定すれば、Djangoのモデルがそのまま使える。

(問題)DBの再構築に時間がかかり、計画停止が必要。

案2:独自クエリ―にて実装する

ORMに頼らずに、自分でクエリ―を書く実装にする。クエリの性能面等を自分でコントロールできる。

(問題)コードとテストが増える。

案3:Modelを拡張する

Djangoプロジェクトでも以前に検討されたようだが、最近はどうなっているかは、よく分からない。

https://code.djangoproject.com/ticket/373

https://code.djangoproject.com/wiki/MultipleColumnPrimaryKeys#CurrentStatus

色々、挑戦した人もいるみたいだが、結局、どれも未完か?

とりあえず、自分でモデルを拡張することで、自分の仕事で使えるように、Modelのサブクラスを作成する。とりあえずは、Djangoのすべての機能をカバーする必要はないので、あくまでも自分が必要とする単体モデルのINSERT,UPDATE,DELETEがカバーできるものならできそう。

(問題)現状のModelのコードを理解する必要がある。

案4:別のORMを作る

モデルまわりのコードを見たが、少しスパゲッティな感じもした。ざっと実装を調べていて感じたのは、Model、Manager、QuerySetでの役割がきれいに分かれてないように見受けられた。複合キー対応も含めて、簡素な自分のORM作るほうがいいような気もする。

(問題)そんな時間は無い!

案5:既にある別のORMを使う

以下は、主キーではなくて、あくまで複合外部キーのためか?

https://github.com/Arisophy/django-composite-foreignkey

(問題)適切なものが見つからない

2.結論

今のプロジェクトは、Ruby On Railsで作られたレガシーなシステムを、徐々にDjangoに移行しているところで、DBに手を加えたくない。

結論として、案2と案3で進めることにした。

INSERT,UPDATE,DELETEについては、案3でModelの拡張クラスで対応する。実際に作ってみたら、少ないコードで、いい感じで動いている。このままその拡張モデルを使いながら、プロジェクトを進めていく。

SELECTについては、そもそも、すべてORMに頼るわけにはいかないので、独自クエリ―も利用する。

拡張クラスをどう作ったかは、後日、公開します。

(2021/01/29)

以下で、パッケージを公開しました。

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

(2021/06/06)

パッケージの日本語説明

(2021/03/16)

Djangoのticket373についての自分の見解は以下。