くりーむわーかー

プログラムとか。作ってて ・試しててハマった事など。誰かのお役に立てば幸いかと。 その他、いろいろエトセトラ。。。

動的

Python Djangoで動的にモデルを作成

Djangoのモデルはすごく便利なのですが、

固定の定義を作らないとダメなので、

何かの設定とかに応じてモデルを作りたい場合はちょっと使えない。

SQLを自前で書いてゴニョゴニョしてもいいんだけど、

ORMは出来ればDjangoの使いたいのでどうにかしたい。

ということで、モデルを動的に作りたくなった場合。

あと、動的に作ったモデルのテーブルも動的に作りたい場合についても。

参考:https://code.djangoproject.com/wiki/DynamicModels

※すごく古いのでちょっと微妙かも。。。

まず、動的にモデル作る本体は下の感じ。

from django.apps import apps
from django.db import models

def create_model_class(cls, name: str, fields=None, app_label: str = "", module: str = "", options=None):
    """指定されたモデルのClassを作成する"""
    class Meta(object):
        pass
    if app_label:
        setattr(Meta, "app_label", app_label)
    if options is not None:
        for key, value in options.items():
            setattr(Meta, key, value)
    attrs = {"__module__": module, "Meta": Meta}
    if fields:
        attrs.update(fields)
    model = type(name, (models.Model,), attrs)
    return model

普通にクラス定義するときにやってる事を、コードで書いてるだけ。肝は↓のとこ。

type(name, (models.Model,), attrs)

models.Modelを継承したクラス定義にする。

で、どう使うかというと↓の感じ。

app_label = "app"
model_name = "DynamicModelName"
ops = {"db_table": "table_name"}
fields = {}
fields["id"] = models.AutoField(primary_key=True, null=False)
fields["test01"] = models.IntegerField(null=False, default=0)
fields["test02"] = models.CharField(null=False, max_length=255, default="")
ret = create_model_class(
    name=model_name,
    app_label=app_label,
    module=".".join([app_label, model_name]),
    options=ops,
    fields=fields
)

obj = ret()
obj.test01 = 123
obj.test02 = "abc"
obj.save()

q = ret.objects.all()
q.values()

ただ、動的に作ったモデルはDBにはまだテーブル作られてないので、

save()とか、objects.all()とかはもちろん動ない。

テーブルをこの作ったモデル定義が作成する場合はschema-editorを使う。

こんな感じ。

from django.db import connections

connection = connections[con_label]
with connection.schema_editor(collect_sql=False, atomic=True) as se:
    se.create_model(model)  # ここでCreate Tableが発行される

SQLだけ拾いたいときは下の感じ

with connection.schema_editor(collect_sql=True, atomic=True) as se:
    se.create_model(model)
    "\n".join(se.collected_sql)

collect_sqlをTrueにすると、操作に対してのSQLがcollected_sqlのlistに

たまってくので、最後に「"\n".join(se.collected_sql)」で全部取るみたいな。

collect_sqlをTrueにした場合は、create_model()とかしても、SQLは発行されない。

あと、作ったモデルは↓の感じで使いまわせる。

from django.apps import apps

django_model_name = model_name.lower()
if django_model_name in apps.all_models[app_label].keys():
    ret = apps.all_models[app_label][django_model_name]

q = ret.objects.all()
q.values()

「apps.all_models」がDjangoがmodel定義を全部キャッシュしてある場所。

削除したい場合はここから消せばいい気がするんだけど、

ちゃんと調べてないので、要調査。今のとこ使い終わった場合は↓の感じで消す。

def unload(cls, app_label, model_name):
    django_model_name = model_name.lower()
    if app_label in apps.all_models:
        mdic = apps.all_models[app_label]
        if django_model_name in mdic.keys():
            mdic.pop(django_model_name)
            apps.clear_cache() # これないとapps.get_models()から消えてない

あと、外部キー制約がどうもうまくいかなかった。

他にも上手くいかないオプションはあると思うのですが、

単純なテーブルのORMやるだけなら十分。。。

と、ここまで書いてて、こんなパッケージがある事に気づいた。

後でコード見ておこ。。。

C# ラムダ式を動的に作る

DBとのやり取りをEF使って、Linqとかラムダ式でやってる場合、 固定的な奴はいいんだけど、やっぱ動的な条件付けが必要になることがある。

SQLを直で書いてもいいと思うんだけど、せっかくなのでラムダ式も 汎用的にやりたい。いきなり書くとわけわかんないコードになるので、 汎用的にやるために必要な部分をテスト的に書いたものが↓。

いちをこんな感じのある程度複雑なクエリを投げる想定

select * from 台帳
where 
(
	(
	顧客名 = 'お客さん1'
	OR
	顧客名 = 'お客さん2'
	)
	AND 
	(日付 >= '2016-10-01')
)
OR
(
	(
		業務 in ('業務1','業務2','業務3')
		AND
		媒体 = 'メール'
	)
	AND 
	(日付 >= '2016-10-01')
)

で、ソース。

using System.Linq.Expressions;
using System.Reflection;

public List<HogeTable> wkTest()
{
    var predList1 = new List<Expression>();
    var predList2 = new List<Expression>();
    var predList3 = new List<Expression>();
    var param = Expression.Parameter(typeof(HogeTable), "p");

    var left1 = Expression.PropertyOrField(param, "顧客名");
    var left2 = Expression.PropertyOrField(param, "業務");
    var left3 = Expression.PropertyOrField(param, "媒体");
    var left4 = Expression.PropertyOrField(param, "日付");

    //in句用のList
    List wkConstList = new List<string>();
    wkConstList.Add("業務1");
    wkConstList.Add("業務2");
    wkConstList.Add("業務3");

    //条件用の値準備
    var right1 = Expression.Constant("お客さん1");
    var right2 = Expression.Constant("お客さん2");
    var right3 = Expression.Constant("メール");
    var right4 = Expression.Constant("2016-10-01");
    var right5 = Expression.Constant(wkConstList, typeof(List<string>));

    predList1.Add(Expression.Equal(left1, right1));
    predList1.Add(Expression.Equal(left1, right2));
    
    //IN句の代用
    MethodInfo Contains = typeof(List<string>).GetMethod("Contains");
    predList2.Add(Expression.Call(right5, Contains, left2));
    predList2.Add(Expression.Equal(left3, right3));
    //文字列の大小比較
    predList3.Add(Expression.GreaterThanOrEqual(Expression.Call(typeof(string), "Compare", null,left4, right4 ), Expression.Constant(0)));

    var tmpbody1 = predList1.Aggregate((l, r) => Expression.MakeBinary(ExpressionType.OrElse, l, r));
    var tmpbody2 = predList2.Aggregate((l, r) => Expression.MakeBinary(ExpressionType.AndAlso, l, r));
    var tmpbody3 = predList3.Aggregate((l, r) => Expression.MakeBinary(ExpressionType.AndAlso, l, r));

    var body1 = Expression.MakeBinary(ExpressionType.AndAlso, tmpbody2, tmpbody3);
    var body2 = Expression.MakeBinary(ExpressionType.AndAlso, tmpbody1, tmpbody3);
    var body3 = Expression.MakeBinary(ExpressionType.OrElse, body2, body1);

    IEnumerable<HogeTable> wkResult;
    wkResult = HogeTables.AsQueryable().Where(Expression.Lambda<Func<HogeTable, bool>>(body3, param));

    return wkResult.ToList();
}

いちを、全部、Expressionで動的に定義してるので、あとは一工夫してあげれば出来上がりという寸法です。

個人的に条件文のネストといいますか、()の深い構造をどう作るのかな~っていうのを試したかった。 あと、Expression使う場合に文字列の大小比較と、IN句に対応する「List.Contains(x.列名)」的な書き方がいけるのか確認しておきたかったのでそれも。。。

上記のソースで発行されるクエリは↓の条件だった。

WHERE 
(
  ([Extent1].[顧客名] IN (N'お客さん1',N'お客さん2')) 
  AND 
  ([Extent1].[日付] >= N'2016-10-01')
) 
OR 
(
  ([Extent1].[業務] IN (N'業務1', N'業務2', N'業務3')) 
  AND
  ([Extent1].[業務] IS NOT NULL) 
  AND
  (N'メール' = [Extent1].[媒体]) 
  AND
  ([Extent1].[日付] >= N'2016-10-01')
)

おおむね、想定通りのクエリ。文字列の大小比較とかいけるか怪しい気がしたけどちゃんと行けてるの。あとOrで繋げた部分も勝手に判断してin句に変えてくれてる。賢い。ただ、「is not null」がじゃっかん気になるところ。。。

で、最初はBlock使わないと条件文のネストしてくれないかと思ってたんだけど、順番つけて普通にAndAlsoとかOrElseで繋げれば勝手にやってくれるっぽ。てか、DB宛てじゃないモデルの場合はBlock使えるんだけど、テーブルが裏にいるEFのモデルだとBlock使わせてくれないのかしら。何かエラーになる。あまし調べてない。。。

あー、Linqの書き方の場合でも上記のExpressionそのままWhereに突っ込めば動く。

問合せ