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

#概要
DjangoのORM機能は素晴らしいが、動的に作成したテーブルや、postgresの特定スキーマ内のテーブル等、少し想定と異なるテーブルに対してModelを作成しようとすると、なかなか情報が少ない。

また、作成済のテーブルを流用し、別のスキーマに同じ定義のテーブルを作成するなど少々込み入ったことをしようとすると、調査に時間がかかり、とたんに開発の手が数日止まってしまう。

この記事は、これら課題に直面し、四苦八苦しながら、以下を実現しようとした記録である。

**＜実現したいこと＞**

- 既存のModelと同じ定義で、スキーマのみが異なるModelを、既存Modelを流用して**動的に**作成したい。
- 作成したModelを使って、作成したModelとは異なるスキーマにテーブルを**動的に**作成したい。
- 作成したModelを使って、**DjangoのORM機能を使って**、検索や登録を行いたい。

要するに、少々こみった要件ではあるが、DjangoのModelクラスを利用したORM機能は秀逸なので、テーブル作成やレコード登録、検索などの恩恵を最大限に受けたいわけで。

#そもそもの動機

## なぜPostgresの標準スキーマ以外のスキーマを使いたいのか

- [PostgreSQL 10.5文書](https://www.postgresql.jp/document/10/html/ddl-schemas.html)に記載されているスキーマ用途の一つである「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](https://code.djangoproject.com/wiki/DynamicModels) を参考に、所望の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とした。

```sql
create schema user01;
```

##Django shellの起動

```sh
$ 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.

```
##モジュールのインポート
続いて、関数を利用するために必要なモジュールのインポートを行う。

```python
from chempre.models.models import Compound
from chempre.models import models 
```

##新規Modelの動的作成
さぁ、いよいよ作成した関数を使って、Modelを動的に作成してみよう。
ここでは、CompoundというModelを流用し、同じ定義を持つmy_compoundというテーブルを、先ほど作成したuser01スキーマに作成する。

```python
model= models.create_model_by_prototype("MyCompound", "user01", "my_compound", Compound)
```

##テーブルの作成
続いて、Modelに対応するテーブルを作成する。今回のModelはmigrationの管理対象外であるため、通常とは異なる作成方法となる。その手順は以下の通りだ。

```python
from django.db import connection
with connection.schema_editor() as schema_editor:
     schema_editor.create_model(model)
```

これでスキーマuser01にテーブルが作成される。実際にPostgresにpsqlで接続してみると確かに作成されている。

##Modelへのデータの登録
続いてデータの登録を行ってみよう。フィールドの指定が面倒だったので、ここではnameフィールドのみ設定したレコードを作成している。

```python
model.objects.create(name='compound name')
```

##Modelへの検索
テーブルが作成されたかどうか検索して確認してみよう。

```python
compound=model.objects.get(id=1)[
print(compound.name)
```
登録されていれば、'compund name'という値が表示されるはずだ。
また、実際にPostgresにpsqlで接続してみるも、確かにレコードが作成されている。

#おわりに
無事、当初掲げていた実現したいことを全て実現することができた。この方法でガンガンModelを量産し、Djangoライフを楽しみたい。
Djangoは、お作法が多く、覚えることも山ほどあるが、この手のフレームワークにありがちな、ガチガチで融通が利かないといったことは全くなく、カスタマイズも柔軟にできそうな印象だ。


#参考文献
- [Specify PostgreSQL Schema in Django](https://stackoverrun.com/ja/q/11408099)
- [Python Djangoで動的にモデルを作成](http://cream-worker.blog.jp/archives/1076337096.html)
- [Dynamic models](https://code.djangoproject.com/wiki/DynamicModels)