将现有模型中 Django 字段的描述复制到新模型中

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()复制字段,但我并没有继续进行下去以找出答案:在类定义中列出的字段实际上并未包含在类对象的属性中。我猜想它们被用于一些元类骗局。
在调试器中查看后,我发现People._meta.local_fields中列出了某些类型的Field对象。然而,这些不仅仅是可以copy.copy()并用于描述另一个模型的简单描述。例如,它们包括一个指向People.model属性。
如何基于现有模型的字段创建新模型的字段描述呢?

1
动态生成新的模型。未来的你想和你说句话... - Ignacio Vazquez-Abrams
什么使用情况需要动态模型? - Collin Grady
@IgnacioVazquez-Abrams 这只是一个简化的示例,但计划生成从主表索引字段的辅助表,类似于How FriendFeed uses MySQL to store schema-less data中描述的内容。大多数情况下将使用主要模型,但某些请求将使用具有最合适索引的模型处理。(次要模型将具有对第一个模型的ForeignKey引用。)这些事实与我的动机相关,但不是核心技术问题;我认为它们会分散注意力。 - Jeremy
该文章讨论了创建新表的内容。Django 模型不是表,而是对数据库的抽象。Django ORM 没有准备好执行您需要完成的任务,以便清晰地实现它。 - Ignacio Vazquez-Abrams
2个回答

6
从调试器和源代码的研究中发现:所有Django模型都使用/db/models/base.py中定义的ModelBase元类。对于模型类定义中的每个字段,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
当我检查新定义的CharField的__dict__属性并将其与已关联模型的CharField进行比较时,我还发现这些是唯一的区别:
- .creation_counter属性对于每个实例都是唯一的。 - .attrname、.column和.model属性在新实例上不存在。 - .name和.verbose_name属性在新实例上为None。
似乎不可能区分手动指定给构造函数的.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)

它运行正常!


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()会引发 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 = ...


因为某些“非用户逻辑扩展模型”想要具有“完全相同的字段”,所以我不应该从我的用户模型中复制重复的字段到抽象类中。为什么呢?因为这与用户模型并不是“直接”相关的,用户模型不应该关心依赖它的内容(除非您想要“扩展用户模型”),因此,它不应该因为其他一些带有其自己的非用户相关逻辑的模型想要具有完全相同的字段而被分离。
相反,您可以这样做:
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')

您还可以创建一些实用程序,以“即时生成”abc类,以避免代码重复:
class UserDataUpdateApplication(
    generate_abc_for_model(
        User,
        fields=['first_name', 'last_name', 'email', 'phone_number'],
        prefix_fields_with='user_'),
    models.Model,
):
    pass


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接