Tech

[Django] admin画面で(も)複数データベースを上手く扱う

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()

 

-Tech
-, , , ,

© 2020 スターレイヴ