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

问题描述 投票:4回答:2

我正在尝试根据现有模型中的字段动态生成新模型。两者都在/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中命名的所有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是否可以工作,但我没有得到足够的结论:类定义中列出的字段实际上并没有作为属性包含在类对象中。我认为它们被用于一些元类恶作剧。

在调试器中查找后,我发现People._meta.local_fields中列出了某种类型的Field对象。然而,这些不仅仅是简单的描述,可以用copy.copy()ed并用于描述另一个模型。例如,它们包括.model属性,指的是People

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

django django-models
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_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)

有用!


2
投票

有复制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)

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

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

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

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 = ...

因此,我不应该从我的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类“on fly”以避免代码重复:

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

© www.soinside.com 2019 - 2024. All rights reserved.