These are materials for DjangoCongress JP 2018 (May 19, 2018).
This document written in Japanese. If you can't read Japanese, please use google translate.
このセッションでは Django のソースコードを参照しています https://github.com/django/django
あらかじめ
git clone git@github.com:django/django.git
を実行して、Django のソースコードを clone しておき、適宜ソースコードを参照すると良いと思います。
- Hajime Nakagami
- https://github.com/nakagami
- Python/Django を書くプログラマー
- 株式会社ビープラウド勤務
- 仕事では Django でアプリケーションコードを書いている
- プライベートでPython のデーターベースドライバーを書いている
- PyCon JP 2016 で、データーベースドライバーについて発表した https://gist.github.com/nakagami/bfbe98d62377f3f4554121ab161ae8c9
- PyCon JP 2017 にNoSQL なデーターベースドライバーについての CfP を出して落選 https://gist.github.com/nakagami/504167b7567ee0ee0548577b4d8471d6
- 最近は、Django のデーターベースバックエンドを書いている
- Django のモデル操作をデーターベースドライバーの呼び出しに変換する層
- Django には PostgreSQL, MySQL, Oracle, SQLite3 の database backend があらかじめ用意されている
- サードパーティー製のデーターベースバックエンドも書けるようになっている https://docs.djangoproject.com/en/2.0/ref/databases/#using-a-3rd-party-database-backend
下図の ORM とPython DB API の間にある層
( http://www.oracle.com/technetwork/articles/dsl/vasiliev-django-100257.html より)
Django のバージョンアップは、 後方互換性に配慮しているが、更新が激しいので、サードパーティー製のデーターベースバックエンドは、 想定した Django のバージョンから乖離すると動作しなくなる。 古くてメンテナンスされてなさそうなデーターベースバックエンドは、新しいバージョンの Django ではうまく動作しないと考えたほうがよい。
Django 2.0 で大き目の変更(機能追加)がおこなわれてしまったので、 Django 1.11 → Django 2.0 の壁は厚い。 https://docs.djangoproject.com/en/2.0/releases/2.0/#database-backend-api
データーベースバックエンドを書くときには、パブリックな API だけでなく、 それを呼び出すプライベートなメソッドも含めて関連する Django のソースコードを読むことになる。
(_ で始まっていないメソッドはパブリックな API なのか?どこからがパブリックなのか?)
Django のソースコードにあるビルトインのデーターベースバックエンドのコードを参考にする。
関連するコードは django/db/backends の下にある。
https://github.com/django/django/tree/master/django/db/backends
基底クラスが django/db/backends/base に定義されていて、それらのクラスを派生して データーベース固有の動作を記述した、データーベース毎のディレクトリがある。
- django.db.backends.base https://github.com/django/django/tree/master/django/db/backends/base
- django.db.backends.mysql https://github.com/django/django/tree/master/django/db/backends/mysql
- django.db.backends.oracle https://github.com/django/django/tree/master/django/db/backends/oracle
- django.db.backends.postgresql https://github.com/django/django/tree/master/django/db/backends/postgresql
- django.db.backends.sqlite3 https://github.com/django/django/tree/master/django/db/backends/sqlite3
(例) BaseDatabaseWrapper を派生してDatabaseWrapper クラスを記述
上図では、メソッド get_new_connection(), create_cursor() しか記述していないが、実際には多くのメソッドがある。
ENGINE に django.db.backends.mysql を指定した場合に django.db.connections の辞書アクセス(っぽい記述)をすると django.db.backends.mysql.DatabaseWrapper のインスタンスが返される。
django.db.utils.ConnectionHandler.__getitem__() https://github.com/django/django/blob/master/django/db/utils.py#L203
(例)以下の、sample.py の 変数 conn には、django.db.backends.mysql.base.DatabaseWrapper のインスタンスが (初めての場合は、 django.db.backends.mysql.DatabaseWrapper のインスタンスが生成されて)入る
settings.py
DATABASES = { 'default': { 'NAME': 'db1', 'ENGINE': 'django.db.backends.mysql', 'HOST': 'localhost', 'USER': 'spam_user', 'PASSWORD': 'spam', }, 'other': { 'NAME': 'db2', 'ENGINE': 'django.db.backends.mysql', 'HOST': 'localhost', 'USER': 'ham_user', 'PASSWORD': 'ham', } }
sample.py
from django.db import connections conn = connections['other'] cur = conn.cursor()
connections['other'].cursor()
で、django.db.base.backends.base.base.BaseDatabaseWrapper.cursor() が呼ばれ
https://github.com/django/django/blob/master/django/db/backends/base/base.py#L253
最終的に django.db.backends.mysql.base.DatabaseWrapper.create_cursor() が呼ばれる
https://github.com/django/django/blob/master/django/db/backends/mysql/base.py#L247
def create_cursor(self, name=None): cursor = self.connection.cursor() return CursorWrapper(cursor)
self.connection は、データーベースドライバーの MySQLdb.connect(...) が返した値。
コード量は多くないので、自分の使っているデーターベースのデーターベースバックエンドのソースは一度見てみると良い。
データーベースバックエンドを書く・・・というのは、 上記のDatabaseWrapper だけでなく必要な BaseXXXX クラスを継承してデーターベース固有の処理を書くということ。 BaseXXXX をオーバーライドして固有の処理を書く必要がないクラスもある。
django.db.backends.base にある以下のファイルの大まかな機能について説明します
- base.py
- client.py
- creation.py
- features.py
- introspection.py
- operations.py
- schema.py
- validation.py
database backend によっては、これらのうち一部のクラスしか派生していない(Base クラスのものをそのまま使っている)ものもあります。
データーベースドライバーの connection オブジェクトのラッパー DatabaseWrapper クラスを定義
例: https://github.com/django/django/blob/master/django/db/backends/mysql/base.py
モデルの Field のタイプ毎に SQL の型を定義
model の オペレーター(XXX__icontains など)に対して、どのような SQL 文を発行するか
そのほかに、最低限 BaseDatabaseWrapper からのオーバーライドが必要なメソッド
- get_new_connection() database driver の connect() を呼んで、 connection オブジェクトを返す
- create_cursor() database driver の connection.cursor() のラッパー
manage.py dbshell で実行されるコマンド(mysql/psql/sqlite3/sqlplus)のラッパー
例: https://github.com/django/django/blob/master/django/db/backends/mysql/client.py
あまり見なくてよい。
django のテストを実行するときにテスト用のデーターベースを作成するための処理。
例: https://github.com/django/django/blob/master/django/db/backends/sqlite3/creation.py
コードを読む場合には重要ではないが、独自の database backend を書く場合は、テストを実行できるようにきちんと書いたほうが良い。
例: https://github.com/django/django/blob/master/django/db/backends/base/features.py
- 機能の有無を示すフラグ変数
- 例外が発生するときのクラス
- 各データーベース固有の書き方になってしまう SQL文の定義
例: https://github.com/django/django/blob/master/django/db/backends/mysql/introspection.py
- データーベースのスキーマから Django の Model, Field を作成するための処理
- description で示される型は、(データーベース)ドライバー毎にマチマチなので、この処理はドライバー毎に書かないといけないはず
- manage.py inspectdb の時に必要
- makemigrations で migration ファイルを作るときに、Django のモデル定義とデーターベースのスキーマの差分を求めるために必要
例: https://github.com/django/django/blob/master/django/db/backends/mysql/operations.py
- モデル操作→データーベース操作に必要な SQL 文の定義
- 標準SQL で対応できそうなものは base/operations.py に書かれているので、データーベース固有のものだけオーバーライドすればよい
- とはいっても、標準SQL で対応できそうなものは全体の一部なので、書かなくてはいけない処理は多い
- operations.py を書くときに、SQL 文(特に、標準SQL文と対象データーベース固有のSQL文の違い)に対する理解が必要になる
- 似ている(または同じ) SQL 文の RDBMS があれば楽 (Postgresql → Redshift の場合や、同じ RDBMS で異なるデーターベースドライバーのバックエンドを書く場合)
- サポートしてない機能 (features.py で定義)によってはオーバーライドする必要のない(使われない) SQL 文もある
例: https://github.com/django/django/blob/master/django/db/backends/mysql/schema.py
- テーブル、カラム、シーケンス、制約を操作するための SQL 文の定義
- BaseDatabaseSchemaEditor で記述されていることも多いが、標準SQLではできないことも多いので、オーバーライドが必要
例: https://github.com/django/django/blob/master/django/db/backends/mysql/validation.py#L37
- データーベース固有のバリデーション
- MySQL では255文字以上の VARCHAR を元にしたフィールドではインデックス張れないのでエラー
- オーバーライドしなくてよい場合が多い。必要になったら考えればよい
データーベースバックエンドに関連するクラスはたくさんあるが、必要な部分だけをオーバーライドすれば良い。 既存の database backend から継承してちょっとした改造を加えることもできる。
django-postgres-readonly https://github.com/opbeat/django-postgres-readonly
普通は、書く必要はない。 自分の使いたいデーターベースの database backend を書くよりは、 Django にビルトインされ十分にテストされた database backend とデーターベースを使うことをお勧めする。
Django には大量のテストコードがある
Django は pure python で書かれているが、データーベースドライバー psycopg2 や mysqlclient は C拡張の部分がある。 データーベースドライバーのインストールは、 Django をインストールする時の最大のはまりどころ。
pure python のデーターベースドライバー向けのデーターベースバックエンドがあると、インストールが楽。
MySQL の場合は pure python のドライバー PyMySQL をインストールして AppConfig.ready() https://docs.djangoproject.com/en/2.0/ref/applications/#django.apps.AppConfig.ready に以下のようなコードを書くと django.db.backends.mysql が使える
import pymysql pymysql.install_as_MySQLdb()
install_as_MySQLdb() の中で sys.modules を細工して、 import MySQLdb
で、pymysql がimport されるようにしている。
https://github.com/PyMySQL/PyMySQL/blob/master/pymysql/__init__.py#L116
これは、公式には保証されていないものの一般的に知られているやりかた。 pymysql が MySQLdb (mysqlclient) と同じ挙動なのか? まったく同じということはないはずだが、何かはまりどころはないのか?
知っている人がいたら教えて欲しい。
Firebird https://firebirdsql.org/
django-firebird が Django 1.8 までサポートしていたが・・・ https://pypi.python.org/pypi/django-firebird/ (現在は Django 1.11 対応中。後述。)
https://github.com/nakagami/django-cymysql
CyMySQL の Python3 対応に役立った(大量のテストコードがある)
ひとつのリリースパッケージ (tarball) で複数のバージョンの Django に対応しようとしたけど、Django が変化し過ぎで最近挫折した
Django のバージョンに対応したバージョンの django-cymysql をインストールする https://github.com/nakagami/django-cymysql#installation
エゴサーチすると自分には読めない言語の Mailing List のやり取りやブログ記事やyoutube 動画が発見される
- Conectar DJANGO 1.9.7 con MYSQL usando PYTHON 3.4 https://www.youtube.com/watch?v=luCB2C1VSjY
https://github.com/nakagami/django-minipg
- pure python のデータベースドライバー minipg https://github.com/nakagami/minipg を使用
- django.contrib.postgres の機能は使えない https://docs.djangoproject.com/en/2.0/ref/contrib/postgres/
- GeoDjango は使えない https://docs.djangoproject.com/en/2.0/ref/contrib/gis/
- 以前書き始めたが、テストの Success が増えずにリリースが止まっていた
- 最近 Django 2.0 に追随して psycopg2 固有の機能を使っているところ以外は動くようになった(psycopg2 固有の機能を活用しているところは多い)
https://github.com/nakagami/djfirebirdsql
- Firebird 4.0 (master branch HEAD, unreleased) https://github.com/FirebirdSQL/firebird
- pyfirebirdsql https://github.com/nakagami/pyfirebirdsql recent release
- Django (master branch HEAD)
- RENAME TABLE がない
- 一時的に外部キー制約を解除する機能(MySQL のSET foreign_key_checks=0) がない
- 時刻の分解能が 1/100秒
- カラムの型を VARCHAR → BLOB, DOUBLE → INTEGER に変更するような SQL文でエラー
- 文字列の最大長が短くなるようなカラムの変更するようなSQL文でエラー
- 外部キー制約の参照カラムはPRIMARY KEY 制約か、UNIQUE KEY 制約が必要
- インデックス(キー制約)の効いたカラムの型を変更する SQL文でエラー
- integer に IDENTITY を付与する SQL文 (IntergerField から AutoIncrementField に変換するときの SQL 文) がわからない
- その他→直せそうなところがあったら Pull Request 送ってほしい
全体として、多少制約はあるものの、使えるかな・・・という状態まできた
- Django に pull request 送った
- django/django#9702
- pyfirebirdsql の cursor に factory を指定できるようにした
- https://github.com/nakagami/pyfirebirdsql/blob/master/firebirdsql/fbcore.py#L523
- pyfirebirdsql のFirebird 4.0 対応 decfloat 対応
- https://www.firebirdnews.org/firebird-support-for-decfloat-will-be-enabled-in-firebird-4-0/
- さらに、そこから派生して Erlang の Firebird ドライバーの decfloat 対応
- https://github.com/nakagami/efirebirdsql
- Django 1.8 のサポートで止まっていた django-firebird の開発が Django 1.11 対応中まで進んだ
- https://www.firebirdnews.org/django-firebird-1-10-has-been-released-work-for-1-11-has-started-in-master-branch/
データーベースごとのSQL文の違いをバックエンド毎に書くようになっていない
例 https://github.com/django/django/blob/master/django/db/models/functions/comparison.py#L5
class Cast(Func): ... def __init__(self, expression, output_field): super().__init__(expression, output_field=output_field) def as_sql(self, compiler, connection, **extra_context): ... def as_mysql(self, compiler, connection): ... def as_postgresql(self, compiler, connection): ...
mysql の場合は as_mysql() が、postgresql の場合は as_postgresql() が、それ以外は as_sql() が呼ばれる。
as_sql() じゃない as_XXX() のXXX の部分は、 DatabaseWrapper の vendor でした文字列 https://github.com/django/django/blob/master/django/db/backends/oracle/base.py#L75 新たな RDBMS (vendor) で使えるようにするためには、データーベースバックエンドの対応だけでなく Django 本体の修正が必要になってしまう。
サードパーティーのデーターベースバックエンドの処理の中でフックして上書きすることはできる https://github.com/nakagami/djfirebirdsql/blob/master/djfirebirdsql/operations.py#L51
... ConcatPair.as_firebirdsql = ConcatPair.as_sqlite Substr.as_firebirdsql = _substr_as_sql StrIndex.as_firebirdsql = _str_index_as_sql Repeat.as_firebirdsql = Repeat.as_oracle ...
- Custom Database Backends
- Django Under The Hood 2016 https://simpleisbetterthancomplex.com/media/2016/11/db.pdf
- Redshift Backend
- PyCon JP 2016 by Takayuki Shimizukawa ( @shimizukawa ) https://pycon.jp/2016/ja/schedule/presentation/48/
Django のデーターベースバックエンドを書くのは、泥臭くサードパーティー製のデーターベースバックエンドを Django のテストが (Django 本体の修正なしに)すべて通るように完璧に書くのは無理そう。大変な割に達成感がないので、 「楽しみのためのプログラミングで何かしたい」 という方には、あまりお勧めしない。
データーベースバックエンドの公式 API ドキュメントは(たぶん)ない。 Django のソースコードが仕様。ソースコードを読む。
いったんリリースできても Django のバージョンアップに追随するのは大変で心が折れそう。
既存のソースコードを読むのは、 python のオブジェクト操作をどのようにSQL文に変換しているかが分かって勉強になる。
SQL 標準や、各 RDBMS の SQL文の仕様や標準準拠度について理解が進む。
- Firebird の標準SQL 準拠度は高い
- Firebird は Oracle に近い
Django への pull request は、簡単に受け入れられる場合もある。楽しい。