[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との相互参照の問題も出てくるという作りがどうも好きになれないので。相互参照を避けるための方法は提供されているが・・・。

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

PHPのバージョンアップ作業中

PHPのバージョンアップのために、別サーバーを立ち上げて従来システムの動作確認をしている。

1.バージョン、パス等の確認

まずは、phpが正しいバージョンを参照しているかの確認。

php -v

ライブラリ等のパスは、以下で確認。

php -i | grep path

2.PEAR

何でcomposerじゃないのかというと、元プログラムの関係で、pearを使っている。

インストールされているパッケージの確認。

pear list

PEAR設定の確認は以下。

pear config-show

パッケージのファイルの確認は以下。HTTP_Request2の場合。

pear list-files HTTP_Request2

[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につめて返すようにした。

Spring+Thymeleafドキュメント

SpringとThymeleafに関してのリンク。

[Spring Boot]

(公式)https://spring.io/projects/spring-boot

[Spring Boot Reference Documentation]

https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/html/

(日本語訳)

https://spring.pleiades.io/spring-boot/docs/current/reference/html/

[Spring Data Reference Documentation]

(Spring Data JPA – リファレンスドキュメント)

https://spring.pleiades.io/spring-data/jpa/docs/current/reference/html

[Thymeleaf]

(Tutorial: Thymeleaf + Spring)https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html

(Tutorial: Using Thymeleaf)https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html

これは日本語版もある。

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf_ja.html