5

我正在尝试根据现有模型中的字段动态生成新模型。两者都定义在/apps/main/models.py. 现有模型如下所示:

from django.db import models

class People(models.Model):
    name = models.CharField(max_length=32)
    age = models.IntegerField()
    height = models.IntegerField()

我有一个包含要复制的字段名称的列表:

target_fields = ["name", "age"]

我想生成一个新模型,其中包含所有在 中命名的字段target_fields,但在这种情况下,它们应该被索引(db_index = True)。

我最初希望我能够遍历其类属性People并用于copy.copy复制其上定义的字段描述。像这样:

from copy import copy

d = {}
for field_name in target_fields:
    old_field = getattr(People, field_name) # alas, AttributeError
    new_field = copy(old_field)
    new_field.db_index = True
    d[field_name] = new_field

IndexedPeople = type("IndexedPeople", (models.Model,), d)

我不确定copy.copy()ing Fields 是否会起作用,但我没有深入了解:类定义中列出的字段实际上并未作为类对象的属性包含在内。我假设它们被用于一些元类恶作剧。

在调试器中四处寻找之后,我发现了某些类型的 Field 对象列在People._meta.local_fields. 但是,这些不仅仅是可以copy.copy()编辑和用于描述另一个模型的简单描述。例如,它们包含.model引用的属性People

如何根据现有模型的字段为新模型创建字段描述?

4

2 回答 2

6

从调试器和源代码中探查:所有 Django 模型都ModelBase使用/db/models/base.py. 对于模型类定义中的每个字段,ModelBase'.add_to_class方法将调用该字段的.contribute_to_class方法。

Field.contribute_to_class在中定义,/db/models/fields/__init__.py它负责将字段定义与特定模型相关联。通过添加属性并使用模型类定义中使用的名称.model调用方法来修改字段。.set_attributes_from_name这反过来又添加了.attname.column属性和集合.name.verbose_name如果需要的话。

当我检查__dict__新定义的属性CharField并将其与CharField已与模型关联的属性进行比较时,我还发现这些是唯一的区别:

  • .creation_counter属性对于每个实例都是唯一的。
  • 新实例.attrname上不存在.column和属性。.model
  • 和属性在新实例上.name.verbose_nameNone

似乎无法区分手动指定给构造函数的.name/.verbose_name属性和自动生成的属性。您需要选择始终重置它们,忽略任何手动指定的值,或者从不清除它们,这将导致它们始终忽略在新模型中给出的任何新名称。我想使用与原始字段相同的名称,所以我不打算碰它们。

知道存在哪些差异后,我使用copy.copy()它来克隆现有实例,然后应用这些更改以使其表现得像一个新实例。

import copy
from django.db import models

def copy_field(f):
    fp = copy.copy(f)

    fp.creation_counter = models.Field.creation_counter
    models.Field.creation_counter += 1

    if hasattr(f, "model"):
        del fp.attname
        del fp.column
        del fp.model

        # you may set .name and .verbose_name to None here

    return fp

鉴于此功能,我使用以下内容创建新模型:

target_field_name = "name"

target_field = People._meta.get_field_by_name(target_field_name)[0]
model_fields = {}

model_fields["value"] = copy_field(target_field)
model_fields["value"].db_index = True
model_fields["__module__"] = People.__module__

NewModel = type("People_index_" + field_name, (models.Model,), model_fields)

有用!

于 2012-09-01T17:03:31.423 回答
3

解决方案

有用于字段复制的构建方式Field.clone()- 解构字段删除任何模型相关引用的方法:

    def clone(self):
        """
        Uses deconstruct() to clone a new copy of this Field.
        Will not preserve any class attachments/attribute names.
        """
        name, path, args, kwargs = self.deconstruct()
        return self.__class__(*args, **kwargs)

因此,您可以使用以下实用程序来复制字段,以确保您不会意外影响要复制的模型的源字段:

def get_field(model, name, **kwargs):
    field = model._meta.get_field(name)
    field_copy = field.clone()
    field_copy.__dict__.update(kwargs)
    return field_copy

也可以传递一些常规的 kwargs,如 verbose_name 等:

def get_field_as_nullable(*args, **kwargs):
    return get_field(*args, null=True, blank=True, **kwargs)

不适用于模型定义内的 m2m 字段。(模型定义上的 m2m.clone() raises AppRegistryNotReady: Models aren't loaded yet

为什么用这个而不是抽象模型?

嗯,视情况而定。有时您不需要继承,但实际需要复制字段。什么时候?例如:

我有一个用户模型和模型,它代表一个用于用户数据更新的应用程序(用户数据更新请求的文档):

class User(models.Model):
    first_name = ...
    last_name = ...
    email = ...
    phone_number = ...
    birth_address = ...
    sex = ...
    age = ...
    representative = ...
    identity_document = ...


class UserDataUpdateApplication(models.Model):
    # This application must ONLY update these fields.
    # These fiends must be absolute copies from User model fields.
    user_first_name = ...
    user_last_name = ...
    user_email = ...
    user_phone_number = ...

因此,由于某些其他非用户逻辑扩展模型希望具有完全相同的字段,因此我不应该将重复字段从我的 User 模型执行到抽象类。为什么?因为它与 User 模型没有直接关系 - User 模型不应该关心依赖于它的东西(不包括您想要扩展 User 模型的情况),所以它不应该被分开,因为它自己的一些其他模型是非 User相关逻辑希望具有完全相同的字段。

相反,您可以这样做:

class UserDataUpdateApplication(models.Model):
    # This application must ONLY update these fields.
    user_first_name = get_field(User, 'first_name')
    user_last_name =  get_field(User, 'last_name')
    user_email =  get_field(User, 'user_email')
    user_phone_number =  get_field(User, 'phone_number')

您还可以制作 som util 来“即时”生成一些 abc 类以避免代码重复:

class UserDataUpdateApplication(
    generate_abc_for_model(
        User,
        fields=['first_name', 'last_name', 'email', 'phone_number'],
        prefix_fields_with='user_'),
    models.Model,
):
    pass

于 2019-02-27T09:44:51.733 回答