概要
DjangoのORM機能は素晴らしいが、動的に作成したテーブルや、postgresの特定スキーマ内のテーブル等、少し想定と異なるテーブルに対してModelを作成しようとすると、なかなか情報が少ない。
また、作成済のテーブルを流用し、別のスキーマに同じ定義のテーブルを作成するなど少々込み入ったことをしようとすると、調査に時間がかかり、とたんに開発の手が数日止まってしまう。
この記事は、これら課題に直面し、四苦八苦しながら、以下を実現しようとした記録である。
<実現したいこと>
- 既存のModelと同じ定義で、スキーマのみが異なるModelを、既存Modelを流用して動的に作成したい。
- 作成したModelを使って、作成したModelとは異なるスキーマにテーブルを動的に作成したい。
- 作成したModelを使って、DjangoのORM機能を使って、検索や登録を行いたい。
要するに、少々こみった要件ではあるが、DjangoのModelクラスを利用したORM機能は秀逸なので、テーブル作成やレコード登録、検索などの恩恵を最大限に受けたいわけで。
そもそもの動機
なぜPostgresの標準スキーマ以外のスキーマを使いたいのか
- PostgreSQL 10.5文書に記載されているスキーマ用途の一つである「1つのデータベースを多数のユーザが互いに干渉することなく使用できるようにする」を実現したいためである。今回、いろんなユーザが利用できる解析サービスの実現を目指しているが、ユーザのデータ同士の干渉をなくしたいため、同じテーブル構成のスキーマをユーザ毎に用意したいのだ。
なぜ既存テーブル・既存Modelを流用したいのか。
- ユーザ登録がされた時点で、そのユーザ用のスキーマや、そのスキーマの中のテーブルを動的に作りたい。この時、わざわざCreate文を用意することなく、既に定義済のModelから作りたいのである。だってCreate文とModelのソースでDDLの二重管理になるじゃん。
環境
今回の環境は以下のとおりである。
- python 3.7.6
- django 3.0.8
- Postgres 12
やり方
結論からいうと、参考文献の応用で実現できた。以下に詳細を記載する。
流用元となるModelの定義
まず、以降の説明のため、流用元となるModelの例を以下の通りとする。
class Compound(models.Model):
name = models.CharField(max_length=1024)
smiles = models.CharField(max_length=4000, null=True)
standard_inchi = models.CharField(max_length=4000, default=None, null=True)
mol = models.TextField(null=True)
image = models.BinaryField(null=True)
流用元のModelは、Djangoのmigration管理下にあるため、makemigration, migrateによりModelに対するテーブルを作成できるはずだ。そして、特別な設定をしない限り、そのテーブルはpublicスキーマに作成される。
Modelを動的に作成する関数を作成
次に今回の肝となる関数を説明する。
通常Djangoでは、あらかじめModelクラスをソースにベタうちして作っておく必要があるが、今回はDynamic models を参考に、所望のModelクラスを動的に作成する関数を用意した。
具体的は、スキーマ、テーブル名、流用元のModelクラスを指定してあらたなModelを作成できるようにした。ソースは以下のとおりである。
なお、関数のためどこに作成してもよいが、今回流用元と同じモジュールに定義している。
def create_model_by_prototype(model_name, schema_name, table_name, prototype):
import copy
class Meta:
pass
setattr(Meta, "db_table", schema_name + "\".\"" + table_name)
fields = {}
for field in prototype._meta.fields:
fields[field.name] = copy.deepcopy(field)
attrs = {'__module__': "app.models.models", 'Meta': Meta}
attrs.update(fields)
model = type(model_name, (models.Model,), attrs)
return model
解説
具体的な利用例は後で見ていくこととし、ソースについて簡単に解説する。
- 引数として、schema_nameにはテーブルを作成するスキーマ、table_nameにはテーブル名、prototypeにはひな型とするModelクラスを指定する。
-
Meta
というクラスに、モデルを生成するための属性を設定していく -
setattr
で、Modelの管理するテーブル名を表す属性"db_table"に対し、引数で指定されたテーブル名を設定している -
for field in prototype._meta.fields:
というループで、流用元のModelのfieldを、今回作成するモデルのfieldに設定するための準備を行っている。 - 最終的に、
type(model_name, (models.Model,), attrs)
で、準備した属性情報を元に、新たなModelクラスを生成し、返却している。
ハンズオン
さて、作成した関数を用いた関数の利用イメージを、django shellによるハンズオンで見ていこう。
流用元となるModel、テーブルの作成
これは、Django の makemigrations, migrateを実行するだけなので省略する。
新規スキーマの作成
新たなテーブルを格納するためのスキーマは、今回面倒だったので直接Postgresに接続し作成した。スキーマ名をuser01とした。
create schema user01;
Django shellの起動
$ python manage.py shell
Python 3.7.6 | packaged by conda-forge | (default, Jun 1 2020, 18:11:50) [MSC v.1916 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 7.16.1 -- An enhanced Interactive Python. Type '?' for help.
モジュールのインポート
続いて、関数を利用するために必要なモジュールのインポートを行う。
from chempre.models.models import Compound
from chempre.models import models
新規Modelの動的作成
さぁ、いよいよ作成した関数を使って、Modelを動的に作成してみよう。
ここでは、CompoundというModelを流用し、同じ定義を持つmy_compoundというテーブルを、先ほど作成したuser01スキーマに作成する。
model= models.create_model_by_prototype("MyCompound", "user01", "my_compound", Compound)
テーブルの作成
続いて、Modelに対応するテーブルを作成する。今回のModelはmigrationの管理対象外であるため、通常とは異なる作成方法となる。その手順は以下の通りだ。
from django.db import connection
with connection.schema_editor() as schema_editor:
schema_editor.create_model(model)
これでスキーマuser01にテーブルが作成される。実際にPostgresにpsqlで接続してみると確かに作成されている。
Modelへのデータの登録
続いてデータの登録を行ってみよう。フィールドの指定が面倒だったので、ここではnameフィールドのみ設定したレコードを作成している。
model.objects.create(name='compound name')
Modelへの検索
テーブルが作成されたかどうか検索して確認してみよう。
compound=model.objects.get(id=1)[
print(compound.name)
登録されていれば、'compund name'という値が表示されるはずだ。
また、実際にPostgresにpsqlで接続してみるも、確かにレコードが作成されている。
おわりに
無事、当初掲げていた実現したいことを全て実現することができた。この方法でガンガンModelを量産し、Djangoライフを楽しみたい。
Djangoは、お作法が多く、覚えることも山ほどあるが、この手のフレームワークにありがちな、ガチガチで融通が利かないといったことは全くなく、カスタマイズも柔軟にできそうな印象だ。