LoginSignup
9
13

More than 3 years have passed since last update.

Djangoでスキーマに動的にテーブル作成、動的にModel生成

Last updated at Posted at 2020-08-11

概要

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の例を以下の通りとする。

app/models/model.py
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を作成できるようにした。ソースは以下のとおりである。
なお、関数のためどこに作成してもよいが、今回流用元と同じモジュールに定義している。

app/models/model.py
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は、お作法が多く、覚えることも山ほどあるが、この手のフレームワークにありがちな、ガチガチで融通が利かないといったことは全くなく、カスタマイズも柔軟にできそうな印象だ。

参考文献

9
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
13