PythonのWebフレームワーク Django は基本的には単一のデータベースとやり取りを行って処理を行います。
ほとんどの開発現場ではひとつのサービスDBと接続するのが一般的だと思われますが、この記事で扱うように、複数のサービス用DBからデータを引っ張ってくることも考えられます。
この記事では例として、Admin画面(Django admin site)で複数のデータベースのマネジメントを行えるようにします。
実装はめちゃめちゃシンプルで分かりやすいです!
Djangoで複数データベースを使うハードル
何も設定せずに複数のデータベースにまたがるようなモデルを使用しようとすると、ProgrammingErrorなどの例外がraiseされます。
1146, "Table 'prod.price' doesn't exist"
これは、Djangoはdefaultで設定されているデータベースを参照するため。
たとえ別のデータベースを意図してmodelsを作っても、Djangoはすべてのモデルにおいて、settings.pyで設定したDefaultのデータベースを参照しようとします。
なので、modelsのテーブルに応じて、適切なデータベースを選択できるようDjangoに指示を出す必要があります。
方法1. Routerを作成する
最もシンプルかつ確実な方法です。
models.Modelでモデル定義をすることに加え、データベースのスイッチができるよう Router を定義します。
例えば下記のようなモデルを定義し、defaultではないデータベース上のテーブルと対応させたいとしましょう。
project/appname/models.py
class Price(models.Model): price = models.IntegerField(null=False)
settings.pyのデータベースに関する設定は下記のような感じ。
ここでは例としてMySQLを使用しています。
project/project/settings.py
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'database_name1', 'USER': 'django', 'PASSWORD': 'passw0rd', 'HOST': 'dbhostname', 'PORT': 3306 }, 'prod': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'database_name2', 'USER': 'django', 'PASSWORD': 'passw0rd', 'HOST': 'dbhostname', 'PORT': 3306 } }
このPriceモデル向けにRouterを定義してあげます↓
project/project/router.py
class PriceRouter: def db_for_read(self, model, **hints): if model._meta.app_label == 'price': return 'prod' def db_for_write(self, model, **hints): if model._meta.app_label == 'price': return 'prod' def allow_relation(self, obj1, obj2, **hints): if obj1._meta.app_label == 'price' or \ obj2._meta.app_label == 'price': return True def allow_migrate(self, db, app_label, model=None, **hints): if app_label == 'price': return db == 'prod'
このRouterを使用できるように、settings.pyに追加します。
project/project/settings.py
DATABASE_ROUTERS = ['project.router.PriceRouter']
これにより、DjangoがPriceモデルを参照しようとする際には、"prod"として設定されたデータベースを自動的に見に行くようになります。
この設定はsettings.pyのスコープ(プロジェクト)全体で有効になるので、アプリケーションはもちろん、Adminサイトでも正しいデータベースを参照するようになります。これで複数のデータベースが使えますね!
方法2. ModelAdminのサブクラスの作成
別の方法は、Django公式のドキュメントで紹介されている方法で、admin.ModelAdminのサブクラスを作成し、そこで読み書きするデータベースを書き換えるものです。
まず、ModelAdminのサブクラスとして、例えば下記のようなクラスを作成しておきます(ドキュメントと同じです)。
project/appname/admin.py
class MultiDBModelAdmin(admin.ModelAdmin): using = 'price' def save_model(self, request, obj, form, change): obj.save(using=self.using) def delete_model(self, request, obj): obj.delete(using=self.using) def get_queryset(self, request): return super().get_queryset(request).using(self.using) def formfield_for_foreignkey(self, db_field, request, **kwargs): return super().formfield_for_foreignkey(db_field, request, using=self.using, **kwargs) def formfield_for_manytomany(self, db_field, request, **kwargs): return super().formfield_for_manytomany(db_field, request, using=self.using, **kwargs)
save()やdelete()の対象データベースとしてusingメソッドを使ってpriceデータベースを指定するという内容です。
あらかじめ作成していたPriceモデルをadmin.site.registerするときに、上記クラスを指定してあげます。
project/appname/admin.py
admin.site.register(Price, MultiDBModelAdmin)
これでAdminサイトでも適切なデータベースを参照してくれるようになり、複数データベースを扱えるようになります。
ただし、この方法では外部キーのレコードを削除する際などにうまく行かない(例外となる)場合があります。
Djangoの内部的には、その動作をするときに上述のRouterに問合せが行われているようです。つまり、Routerの記述がなければ、どれだけModelAdminでデータベースを指示していようが、特定の動作ではデータベースの迷子になるケースが出てきてしまいます。
ですので結論としては、特に障害がなければ、まずはRouterを正しく実装しておくのがおすすめです。
追記
ちなみに、読み出すデータベースをクエリのたびに手動で指定することも可能で、一部運用ではこちらのほうが適しているかもしれません。
# 'default' データベースを参照 prices = Price.objects.all() # 'prod' データベースを参照 prices = Price.objects.using('prod').all()