LinuxのPHPからWindowsのSQLServerに接続

Windows上のSQLServerに、Linuxサーバー上のPHPプログラムから接続する手順。

Microsoftがドライバーを提供していて、以下のチュートリアルを参考に作業。

https://learn.microsoft.com/ja-jp/sql/connect/php/installation-tutorial-linux-mac?view=sql-server-ver16

今回のLinuxサーバーはCentOS Stream 9なので、Red Hatの部分を参考に進める。

1)ODBCドライバー

1-1)Linux用ODBCドライバー

https://learn.microsoft.com/ja-jp/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server?view=sql-server-ver15&tabs=redhat18-install%2Calpine17-install%2Cdebian8-install%2Credhat7-13-install%2Crhel7-offline

レポを更新して

curl https://packages.microsoft.com/config/rhel/9.0/prod.repo > /etc/yum.repos.d/mssql-release.repo

必要なものをインストールする。

sudo dnf install msodbcsql18
sudo dnf install mssql-tools18
sudo dnf install unixODBC-devel

1-2)WindowsServer側で接続許可

Windows側で、SQLServerのTCP/IP接続設定と、ファイアーウォールでLinuxサーバーからの接続許可をする。

1-3)ODBC設定

/etc/odbc.iniを編集して、DSNを定義。

https://learn.microsoft.com/ja-jp/sql/connect/odbc/linux-mac/connection-string-keywords-and-data-source-names-dsns?source=recommendations&view=sql-server-ver16

1-4)接続確認

sqlcmdで接続確認。暗号化を有効にしているので、証明書エラーになるのでCオプションを指定する。

/opt/mssql-tools18/bin/sqlcmd -D -C -S (DSN名) -U (ユーザー名) -P  (パスワード)

2)PHPからの接続

PHP用のドライバーについては、以下ページ。

https://learn.microsoft.com/ja-jp/sql/connect/php/overview-of-the-php-sql-driver?view=sql-server-ver16

SQLSRV ドライバーと PDO_SQLSRV ドライバーがあるが、今回はSQLSRV ドライバーを使うこととした。

2-1)SQLSRV ドライバーのインストール

以下コマンドでインストール。

sudo pecl install sqlsrv

ConoHaのVPSのCentOS Stream 9では、足りないものがありmakeエラーとなる。

(エラー)
/bin/ld: cannot find -lltdl
collect2: error: ld returned 1 exit status
make: *** [Makefile:238: sqlsrv.la] Error 1
ERROR: `make' failed

libtool-ltdl-develが足りていないよう。

sudo dnf install libtool-ltdl-devel

ところが、見つからないと出る。どうやら、repoの設定が無効になっているようで、/etc/yum.repos.d/centos.repoを修正して、crbを有効にすることで解決。

:
[crb]
:
enabled=1     ※ここを0から1に変更
  
[crb-debuginfo]
:

2-2)PHPの設定

起動時にドライバーを読み込ませる。

https://learn.microsoft.com/ja-jp/sql/connect/php/loading-the-php-sql-driver?view=sql-server-ver16#loading-the-driver-at-php-startup

/etc/php.d に、以下内容で30-sqlsrv.iniを作成。

extension=sqlsrv.so

2-3)接続確認

サンプルプログラムを修正して、ブラウザから確認する。

https://learn.microsoft.com/ja-jp/sql/connect/php/installation-tutorial-linux-mac?view=sql-server-ver16#sqlsrv-example

SQLSRV ドライバーのAPIについては、以下ページ。

(APIリファレンス)

https://learn.microsoft.com/ja-jp/sql/connect/php/sqlsrv-driver-api-reference?view=sql-server-ver16

自己証明書を利用しているので、接続オプションにTrustServerCertificateを追加した。

<?php
$serverName = "myServer,9999";  # 9999:PortNo
$connectionOptions = array(
    "TrustServerCertificate" => "yes", # 自己証明書を信頼
    "database" => "myDatabase",
    "uid" => "myUser",
    "pwd" => "myPassword"
);
:

接続オプションについての詳細は以下ページ。

(接続オプション)

https://learn.microsoft.com/ja-jp/sql/connect/php/connection-options?view=sql-server-ver16

本番DBを開発用にマスキング

本番データを、開発環境に落とした後に個人情報部分を書き換えるクエリ。あくまでも開発環境用なので、データマスキングツールを使うまでもないので、簡単なクエリで書き換えるというだけ。以下はPostgreSQLでのサンプル。

1.固定名+連番に書き換え

名前(name)を固定値「試験 太郎」+キー番号にする場合。数値型のキーのcustomer_idを6文字以内(先頭から)の文字列として後ろにつけている。

UPDATE customer SET
 name = '試験 太郎' || CAST(customer_id AS character varying(6))
 WHERE name <> '';

「試験 太郎1」・・・「試験 太郎999999」のような感じ。

2.文字列の一部を書き換え

名前(name)の1文字目、3文字目、5文字目を「〇」に置き換える場合。

UPDATE customer SET
 name =  '〇' || SUBSTR(name, 2, 1) || '〇' || SUBSTR(name, 4, 1) || '〇' || SUBSTR(name, 6)
 WHERE  name <> '';

「山本文左衛門」が「〇本〇左〇門」となる。短い「孫文」のような場合、「〇文〇〇」となるので、まあテストデータなので特に気にしない。

3.電話番号

ハイフンで連結されている電話番号(tel)の市外局番だけはそのままで、残りは「9999-9999」として、「03-9999-9999」というような感じにする。

UPDATE customer SET
 tel =  CAST(SPLIT_PART(tel ,'-', 1) AS character varying(5)) || '-9999-9999'
 WHERE tel <> '';

ハイフンの無い携帯番号(mobile_no)の先頭6文字はそのままで、後ろ5文字を「99999」とする例で、「08012399999」という感じになる。

UPDATE customer SET
 mobile_no = SUBSTR(mobile_no , 1, 6)  || '99999'
 WHERE mobile_no <> '';

4.メールアドレス

ドメインはそのままで、アカウント部分を固定値「test」+キー番号とする場合。

UPDATE customer SET
 mail =  'test' || CAST(id_customer AS character varying(10)) || '@' || CAST(SPLIT_PART(mail ,'@', 2) AS character varying(64))
 WHERE mail <> '';

「yamada_tarou@hogehoge.com」がid_customer=869の場合、「test869@hogehoge.com」となる。

SQLServerExpressの設定

SQLServerExpressインストール後に、最初に構成マネージャー等で設定すること。

1.TCP/IPの有効化

[SQL Server ネットワークの構成] - [SQLEXPRESS のプロトコル]で、[TCP/IP]を右クリックして、[有効にする]に設定。

2.ポートの変更

[TCP/IPのプロパティ] - [IPアドレス]タブで、下の[IP ALL]のポートを任意のポートを設定して、ファイアウォールで指定IPからの接続許可を設定する。

設定後に、 SQLEXPRESSを再起動する。

3.SSMSのインストール

ManageMentStudioをダウロードしてインストールする。

https://docs.microsoft.com/ja-jp/sql/ssms/download-sql-server-management-studio-ssms?view=sql-server-ver15

「使用できる言語」のところで、「日本語」を選んでダウンロードする。

1)DB復元

ManageMentStudio からDBを復元する方法については、以下。

https://docs.microsoft.com/ja-jp/sql/relational-databases/backup-restore/restore-a-database-to-a-new-location-sql-server?view=sql-server-ver15

2)外部からの接続

外部PCからのDB接続には、saユーザーのログインを許可するか別のログインユーザーを作成する。

SQLServerの[プロパティー]-[セキュリティ]の[サーバ認証]で、[SQL Server認証モード と Windows認証モード]に設定して再起動し、SQLServer認証を有効にする。

接続先は2.で3ポートを変更しているので、ManageMentStudio でポート指定をIPアドレスの後にカンマ区切りで指定する。

4.ODBC設定

古い32Bitサーバーアプリを動かすために、32BitのODBCの設定が必要。

https://docs.microsoft.com/ja-JP/troubleshoot/sql/connect/odbc-tool-displays-32-bit-64-bit

32Bit用のODBCコマンドは以下にある。

c:\Windows\SysWOW64\odbcad32

サーバーでポート指定をすること。

[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/

[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そのものに手を入れることも考えてみたい。