Динамично създаване на Django модели с `type`

Имам 20+ MySQL таблици, prm_a, prm_b, ... с една и съща основна структура, но различни имена, и бих искал да ги асоциирам с класове на Django модел, без да пиша всеки един на ръка. И така, чувствайки се амбициозен, реших да опитам ръката си да използвам type() като клас-фабрика:

Следните работи:

def get_model_meta_class(prm_name):
    class Meta:
        app_label = 'myapp'
    setattr(Meta, 'db_table', 'prm_%s' % prm_name)
    return Meta

prm_class_attrs = {
    'foo': models.ForeignKey(Foo),
    'val': models.FloatField(),
    'err': models.FloatField(blank=True, null=True),
    'source': models.ForeignKey(Source),
    '__module__': __name__,
}

###
prm_a_attrs = prm_class_attrs.copy()
prm_a_attrs['Meta'] = get_model_meta_class('a')
Prm_a = type('Prm_a', (models.Model,), prm_a_attrs)

prm_b_attrs = prm_class_attrs.copy()
prm_b_attrs['Meta'] = get_model_meta_class('b')
Prm_b = type('Prm_b', (models.Model,), prm_b_attrs)
###

Но ако се опитам да генерирам моделните класове, както следва:

###
prms = ['a', 'b']
for prm_name in prms:
    prm_class_name = 'Prm_%s' % prm_name
    prm_class = type(prm_class_name, (models.Model,), prm_class_attrs)
    setattr(prm_class, 'Meta', get_model_meta_class(prm_name))
    globals()[prm_class_name] = prm_class
###

Получавам любопитно изключение на реда type() (като се има предвид, че __module__ всъщност е в речника prm_class_attrs):

File ".../models.py", line 168, in <module>
    prm_class = type(prm_class_name, (models.Model,), prm_class_attrs)
  File ".../lib/python2.7/site-packages/django/db/models/base.py", line 79, in __new__
    module = attrs.pop('__module__')
KeyError: u'__module__' 

Така че имам два въпроса: какво не е наред с моя втори подход и дали това изобщо е правилният начин за създаване на моите класови модели?

Добре - благодарение на @Anentropic виждам, че елементите в моя prm_class_attrs речник се изваждат от Python, когато прави класовете. И сега го карам да работи, но само ако направя това:

attrs = prm_class_attrs.copy()
attrs['Meta'] = get_model_meta_class(prm_name)
prm_class = type(prm_class_name, (models.Model,), attrs)

не и ако задам класа Meta като атрибут с

setattr(prm_class, 'Meta', get_model_meta_class(prm_name))

Наистина не знам защо е така, но поне сега работи.


person xnx    schedule 24.11.2014    source източник
comment
защото не правите prm_class_attrs.copy() във вашия for цикъл, така че __modules__ излиза от dict при първата итерация   -  person Anentropic    schedule 24.11.2014
comment
Да - това е. Благодаря!   -  person xnx    schedule 25.11.2014
comment
Нямах представа, че трябва да предоставите __module__! Този въпрос беше наистина полезен!!   -  person Brendan    schedule 26.09.2015


Отговори (3)


Непосредствената причина е, че не правите prm_class_attrs.copy() във вашия for цикъл, така че ключът __modules__ излиза от dict при първата итерация

Що се отнася до това защо това не работи:

setattr(prm_class, 'Meta', get_model_meta_class(prm_name))

...това е свързано с факта, че models.Model на Django има метаклас. Но това е метаклас на Python, който персонализира създаването на моделния клас и няма нищо общо с вътрешния клас Meta на модела Django (който просто предоставя „мета“ информация за модела).

Всъщност, въпреки как изглежда, когато дефинирате класа във вашия models.py, полученият клас няма атрибут Meta:

class MyModel(models.Model):
    class Meta:
        verbose_name = 'WTF'

>>> MyModel.Meta
AttributeError: type object 'MyModel' has no attribute 'Meta'

(Вие можете да получите директен достъп до класа Meta, но с псевдоним MyModel._meta)

Моделът, който дефинирате в models.py, е наистина повече шаблон за клас модел, отколкото действителният клас модел. Ето защо, когато получите достъп до атрибут на поле на екземпляр на модел, вие получавате стойността на това поле, а не самия обект на полето.

Django моделно наследяване може да опрости малко това, което вие правя:

class GeneratedModelBase(models.Model):
    class Meta:
        abstract = True
        app_label = 'myapp'

    foo = models.ForeignKey(Foo)
    val = models.FloatField()
    err = models.FloatField(blank=True, null=True)
    source = models.ForeignKey(Source)

def generate_model(suffix):
    prm_class_name = 'Prm_%s' % prm_name
    prm_class = type(
        prm_class_name,
        (GeneratedModelBase,),
        {
            # this will get merged with the attrs from GeneratedModelBase.Meta
            'Meta': {'db_table', 'prm_%s' % prm_name},
            '__module__': __name__,
        }
    )
    globals()[prm_class_name] = prm_class
    return prm_class

prms = ['a', 'b']
for prm_name in prms:
    generate_model(prm_name)
person Anentropic    schedule 24.11.2014
comment
Навсякъде търсих кратък конкретен пример как да направя това. Благодаря! - person Mike Covington; 30.06.2015

Можете да използвате ./manage.py inspectdb Това ще отпечата файл с модели на Python за DB, към който сочите във вашия settings.py Документация

РЕДАКТИРАНЕ За динамични модели опитайте django-mutant или разгледайте тази връзка

person Mounir    schedule 24.11.2014
comment
Виждам, че това ще работи, ако базата данни е статична и фиксирана, но ако искам да добавя таблица, бих искал кодът да генерира съответния модел в движение. - person xnx; 24.11.2014
comment
@xnx със сигурност ще трябва да напишете куп друг код, за да използвате таблицата в приложението си? - person Anentropic; 24.11.2014

За всеки, който все още се чуди как да направи това, взех гениалния отговор от @Anentropic и го накарах да работи с някои модификации.

Също така промених името на db таблицата на нещо по-подобно на това, което django използва за именуване на таблиците на моделите (appname_lowercaseclassname), като премахна всички неазбучни знаци от името на класа и преобразувах получения низ в малки букви.

Тествах го на Django 2.2.6

def generate_model(class_name):

    clean_name_for_table = ''.join(filter(str.isalpha, class_name)).lower() # Could also use regex to remove all non alphabetic characters.
    db_table_name = f"{__package__}_{clean_name_for_table}"
    prm_class = type(
        class_name,
        (BaseClass,),
        {
            # this will get merged with the attrs from GeneratedModelBase.Meta
            'Meta': type("Meta",(),{'db_table':db_table_name}),
            '__module__': __name__,
        }
    )
    globals()[class_name] = prm_class
    return prm_class
person José R. Sánchez    schedule 19.09.2020