Skip to content

Instantly share code, notes, and snippets.

@nakagami
Last active June 2, 2023 06:22
Show Gist options
  • Save nakagami/098db7387d78ab9c6aa85d77a0eeecf5 to your computer and use it in GitHub Desktop.
Save nakagami/098db7387d78ab9c6aa85d77a0eeecf5 to your computer and use it in GitHub Desktop.
DjangoCongressJP 2018 の発表資料(2018-05-19)

データーベースバックエンドを読む、そして書く(Reading database backend, and writing it)

About this document

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.

https://translate.googleusercontent.com/translate_c?depth=1&tl=en&u=https://gist.github.com/nakagami/098db7387d78ab9c6aa85d77a0eeecf5

はじめに

このセッションでは Django のソースコードを参照しています https://github.com/django/django

あらかじめ

git clone git@github.com:django/django.git

を実行して、Django のソースコードを clone しておき、適宜ソースコードを参照すると良いと思います。

お前誰よ

https://secure.gravatar.com/avatar/e69a5f2a5859ce5f897c2e9dc5158ec5?rating=PG&size=100

BeProud Inc.

http://www.beproud.jp/static/img/logo_beproud.png

https://connpass.com/static/img/common/sitelogo_295x100.png

https://pyq.jp/static/img/logo_square_small.png

Django のデーターベースバックエンドについて

下図の ORM とPython DB API の間にある層

http://www.oracle.com/ocom/groups/public/@otn/documents/digitalasset/113150.gif

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 database backend の概要

Django のソースコードにあるビルトインのデーターベースバックエンドのコードを参考にする。

関連するコードは django/db/backends の下にある。

https://github.com/django/django/tree/master/django/db/backends

基底クラスが django/db/backends/base に定義されていて、それらのクラスを派生して データーベース固有の動作を記述した、データーベース毎のディレクトリがある。

(例) BaseDatabaseWrapper を派生してDatabaseWrapper クラスを記述

https://gist.githubusercontent.com/nakagami/098db7387d78ab9c6aa85d77a0eeecf5/raw/6fce923a8c37fc93a66216cbf4d9a382336f562b/DjangoDatabaseWrapper.png

上図では、メソッド 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 クラスのものをそのまま使っている)ものもあります。

base.py (DatabaseWrapper)

データーベースドライバーの 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() のラッパー

client.py (DatabseClient)

manage.py dbshell で実行されるコマンド(mysql/psql/sqlite3/sqlplus)のラッパー

例: https://github.com/django/django/blob/master/django/db/backends/mysql/client.py

あまり見なくてよい。

creation.py (DatabseCreation)

django のテストを実行するときにテスト用のデーターベースを作成するための処理。

例: https://github.com/django/django/blob/master/django/db/backends/sqlite3/creation.py

コードを読む場合には重要ではないが、独自の database backend を書く場合は、テストを実行できるようにきちんと書いたほうが良い。

features.py (DatabaseFeatures)

例: https://github.com/django/django/blob/master/django/db/backends/base/features.py

  • 機能の有無を示すフラグ変数
  • 例外が発生するときのクラス
  • 各データーベース固有の書き方になってしまう SQL文の定義

introspecton.py (DatabaseIntrospection)

例: https://github.com/django/django/blob/master/django/db/backends/mysql/introspection.py

  • データーベースのスキーマから Django の Model, Field を作成するための処理
  • description で示される型は、(データーベース)ドライバー毎にマチマチなので、この処理はドライバー毎に書かないといけないはず
  • manage.py inspectdb の時に必要
  • makemigrations で migration ファイルを作るときに、Django のモデル定義とデーターベースのスキーマの差分を求めるために必要

operations.py (DatabaseOperations)

例: 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 文もある

schema.py (DatabaseSchemaEditor)

例: https://github.com/django/django/blob/master/django/db/backends/mysql/schema.py

  • テーブル、カラム、シーケンス、制約を操作するための SQL 文の定義
  • BaseDatabaseSchemaEditor で記述されていることも多いが、標準SQLではできないことも多いので、オーバーライドが必要

validation.py (DatabaseValidation)

例: 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 を書くか

普通は、書く必要はない。 自分の使いたいデーターベースの database backend を書くよりは、 Django にビルトインされ十分にテストされた database backend とデーターベースを使うことをお勧めする。

database driver のテストのため

Django には大量のテストコードがある

pure python のデーターベースドライバー向けのデーターベースバックエンドがあるとうれしい

Django は pure python で書かれているが、データーベースドライバー psycopg2 や mysqlclient は C拡張の部分がある。 データーベースドライバーのインストールは、 Django をインストールする時の最大のはまりどころ。

pure python のデーターベースドライバー向けのデーターベースバックエンドがあると、インストールが楽。

それはさておき django.db.backends.mysql で PyMySQL を使う

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) と同じ挙動なのか? まったく同じということはないはずだが、何かはまりどころはないのか?

知っている人がいたら教えて欲しい。

Django が標準でサポートしていないデーターベースが使いたい

Firebird https://firebirdsql.org/

https://firebirdsql.org/img/site/firebird.jpg

django-firebird が Django 1.8 までサポートしていたが・・・ https://pypi.python.org/pypi/django-firebird/ (現在は Django 1.11 対応中。後述。)

事例1: django-cymysql

https://github.com/nakagami/django-cymysql

  • https://github.com/nakagami/CyMySQL を使用

  • CyMySQL の Python3 対応に役立った(大量のテストコードがある)

  • ひとつのリリースパッケージ (tarball) で複数のバージョンの Django に対応しようとしたけど、Django が変化し過ぎで最近挫折した

  • Django のバージョンに対応したバージョンの django-cymysql をインストールする https://github.com/nakagami/django-cymysql#installation

  • エゴサーチすると自分には読めない言語の Mailing List のやり取りやブログ記事やyoutube 動画が発見される

事例2: django-minipg

https://github.com/nakagami/django-minipg

事例3: djfirebirdsql

https://github.com/nakagami/djfirebirdsql

ERROR/FAIL しているテスト

  • 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 送ってほしい

全体として、多少制約はあるものの、使えるかな・・・という状態まできた

Firebird のバックエンド開発の副産物

Django のきれいでないところ

データーベースごとの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
...

そのほかのデーターベースバックエンドについての発表

最後に

Django のデーターベースバックエンドを書くのは、泥臭くサードパーティー製のデーターベースバックエンドを Django のテストが (Django 本体の修正なしに)すべて通るように完璧に書くのは無理そう。大変な割に達成感がないので、 「楽しみのためのプログラミングで何かしたい」 という方には、あまりお勧めしない。

データーベースバックエンドの公式 API ドキュメントは(たぶん)ない。 Django のソースコードが仕様。ソースコードを読む。

いったんリリースできても Django のバージョンアップに追随するのは大変で心が折れそう。

既存のソースコードを読むのは、 python のオブジェクト操作をどのようにSQL文に変換しているかが分かって勉強になる。

SQL 標準や、各 RDBMS の SQL文の仕様や標準準拠度について理解が進む。

  • Firebird の標準SQL 準拠度は高い
  • Firebird は Oracle に近い

Django への pull request は、簡単に受け入れられる場合もある。楽しい。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment