编写自定义模型字段 — Django 文档

来自菜鸟教程
Django/docs/3.2.x/howto/custom-model-fields
跳转至:导航、​搜索

编写自定义模型字段

简介

模型参考 文档解释了如何使用 Django 的标准字段类 - CharFieldDateField 等。 出于许多目的,这些类就是您所需要的。 但是,有时 Django 版本无法满足您的确切要求,或者您希望使用与 Django 附带的字段完全不同的字段。

Django 的内置字段类型并未涵盖所有可能的数据库列类型——仅涵盖常见类型,例如 VARCHARINTEGER。 对于更模糊的列类型,例如地理多边形,甚至是用户创建的类型,例如 PostgreSQL 自定义类型 ,您可以定义自己的 Django Field 子类。

或者,您可能有一个复杂的 Python 对象,它可以以某种方式序列化以适应标准数据库列类型。 这是 Field 子类将帮助您将对象与模型一起使用的另一种情况。

我们的示例对象

创建自定义字段需要注意细节。 为了让事情更容易理解,我们将在整个文档中使用一个一致的例子:在 Bridge 的一手牌中包装一个表示发牌的 Python 对象。 不用担心,您不必知道如何玩桥牌即可遵循此示例。 你只需要知道 52 张牌平均分配给四名玩家,他们传统上称为 northeastsouthwest ]。 我们的类看起来像这样:

class Hand:
    """A hand of cards (bridge style)"""

    def __init__(self, north, east, south, west):
        # Input parameters are lists of cards ('Ah', '9s', etc.)
        self.north = north
        self.east = east
        self.south = south
        self.west = west

    # ... (other possibly useful methods omitted) ...

这是一个普通的 Python 类,与 Django 无关。 我们希望能够在我们的模型中做这样的事情(我们假设模型上的 hand 属性是 Hand 的一个实例):

example = MyModel.objects.get(pk=1)
print(example.hand.north)

new_hand = Hand(north, east, south, west)
example.hand = new_hand
example.save()

就像任何其他 Python 类一样,我们对模型中的 hand 属性进行赋值和检索。 诀窍是告诉 Django 如何处理保存和加载这样的对象。

为了在我们的模型中使用 Hand 类,我们 根本不需要 更改这个类。 这是理想的,因为这意味着您可以轻松地为无法更改源代码的现有类编写模型支持。

笔记

您可能只想利用自定义数据库列类型并将数据作为模型中的标准 Python 类型处理; 例如,字符串或浮点数。 这个案例类似于我们的 Hand 示例,我们会在进行过程中注意任何差异。


背景理论

数据库存储

让我们从模型字段开始。 如果将其分解,模型字段提供了一种获取普通 Python 对象(字符串、布尔值、datetime 或更复杂的东西(如 Hand])并将其转换为格式或从格式转换的方法)这在处理数据库时很有用。 (这种格式对于序列化也很有用,但正如我们稍后将看到的,一旦您控制了数据库端,这会更容易)。

模型中的字段必须以某种方式转换以适应现有的数据库列类型。 不同的数据库提供不同的有效列类型集,但规则仍然相同:这些是您必须使用的唯一类型。 您要存储在数据库中的任何内容都必须适合其中一种类型。

通常,您要么编写 Django 字段以匹配特定的数据库列类型,要么需要一种方法将数据转换为字符串。

对于我们的 Hand 示例,我们可以通过按预定顺序将所有卡片连接在一起,将卡片数据转换为 104 个字符的字符串 - 例如,首先是所有 north 卡片,然后eastsouthwest 卡。 因此 Hand 对象可以保存到数据库中的文本或字符列中。


字段类有什么作用?

Django 的所有字段(当我们在本文档中说 fields 时,我们总是指模型字段而不是 表单字段 )都是 django.db.models.Field[ X176X]。 Django 记录的关于字段的大部分信息对所有字段都是通用的——名称、帮助文本、唯一性等等。 存储所有这些信息由 Field 处理。 我们稍后会详细介绍 Field 可以做什么; 现在,可以说一切都来自 Field,然后自定义类行为的关键部分。

意识到 Django 字段类不是存储在模型属性中的内容,这一点很重要。 模型属性包含普通的 Python 对象。 创建模型类时,您在模型中定义的字段类实际上存储在 Meta 类中(具体如何完成的细节在这里并不重要)。 这是因为当您只是创建和修改属性时不需要字段类。 相反,它们提供了在属性值与存储在数据库中或发送到 序列化器 的内容之间进行转换的机制。

创建您自己的自定义字段时请记住这一点。 您编写的 Django Field 子类提供了以各种方式在 Python 实例和数据库/序列化器值之间进行转换的机制(例如,存储值和使用值进行查找之间存在差异)。 如果这听起来有点棘手,请不要担心 - 在下面的示例中它会变得更清楚。 请记住,当您需要自定义字段时,您通常会最终创建两个类:

  • 第一类是您的用户将操作的 Python 对象。 他们会将它分配给模型属性,他们将从它中读取以用于显示目的,诸如此类。 这是我们示例中的 Hand 类。
  • 第二个类是 Field 子类。 这个类知道如何在它的永久存储形式和 Python 形式之间来回转换你的第一个类。


编写字段子类

在规划您的 Field 子类时,首先考虑您的新字段与现有的 Field 类最相似。 您能否将现有的 Django 字段子类化并为自己节省一些工作? 如果没有,您应该继承 Field 类,所有内容都从该类继承。

初始化您的新字段是将特定于您的情况的任何参数与公共参数分开并将后者传递给 Field(或您的父类)的 __init__() 方法的问题。

在我们的示例中,我们将调用我们的字段 HandField。 (将您的 Field 子类称为 <Something>Field 是个好主意,因此很容易将其识别为 Field 子类。)它的行为与任何现有字段不同,因此我们将直接从 Field 子类化:

from django.db import models

class HandField(models.Field):

    description = "A hand of cards (bridge style)"

    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 104
        super().__init__(*args, **kwargs)

我们的 HandField 接受大多数标准字段选项(参见下面的列表),但我们确保它具有固定长度,因为它只需要容纳 52 个卡值加上它们的花色; 共 104 个字符。

笔记

Django 的许多模型字段都接受它们不做任何事情的选项。 例如,您可以将 editableauto_now 传递给 django.db.models.DateField,它会忽略 editable 参数(auto_now 被设置意味着 editable=False)。 在这种情况下不会引发错误。

这种行为简化了字段类,因为它们不需要检查不必要的选项。 他们将所有选项传递给父类,然后不再使用它们。 您是希望字段对它们选择的选项更严格,还是使用当前字段的更宽松行为取决于您。


Field.__init__() 方法采用以下参数:

上面列表中没有解释的所有选项都具有与普通 Django 字段相同的含义。 有关示例和详细信息,请参阅 字段文档

现场解构

编写 __init__() 方法的对应点是编写 deconstruct() 方法。 在 模型迁移 期间使用它来告诉 Django 如何获取新字段的实例并将其简化为序列化形式 - 特别是传递给 __init__() 的参数以重新创建它.

如果您没有在继承自的字段之上添加任何额外选项,则无需编写新的 deconstruct() 方法。 但是,如果您要更改 __init__() 中传递的参数(就像我们在 HandField 中一样),则需要补充传递的值。

deconstruct() 返回一个包含四项的元组:字段的属性名称、字段类的完整导入路径、位置参数(作为列表)和关键字参数(作为 dict)。 请注意,这与用于自定义类 deconstruct() 方法 不同,后者返回三个事物的元组。

作为自定义字段作者,您无需关心前两个值; 基础 Field 类具有计算字段属性名称和导入路径的所有代码。 但是,您必须关心位置和关键字参数,因为这些很可能是您正在更改的内容。

例如,在我们的 HandField 类中,我们总是在 __init__() 中强制设置 max_length。 基类 Field 上的 deconstruct() 方法将看到这一点并尝试在关键字参数中返回它; 因此,我们可以将其从关键字参数中删除以提高可读性:

from django.db import models

class HandField(models.Field):

    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 104
        super().__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        del kwargs["max_length"]
        return name, path, args, kwargs

如果添加新的关键字参数,则需要在 deconstruct() 中编写代码,将其值自己放入 kwargs。 当不需要重建字段的状态时,您还应该省略 kwargs 中的值,例如使用默认值时:

from django.db import models

class CommaSepField(models.Field):
    "Implements comma-separated storage of lists"

    def __init__(self, separator=",", *args, **kwargs):
        self.separator = separator
        super().__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        # Only include kwarg if it's not the default
        if self.separator != ",":
            kwargs['separator'] = self.separator
        return name, path, args, kwargs

更复杂的示例超出了本文档的范围,但请记住 - 对于您的 Field 实例的任何配置,deconstruct() 必须返回您可以传递给 __init__ 以重建该状态的参数。

如果您为 Field 超类中的参数设置新的默认值,请特别注意; 您想确保它们始终包含在内,而不是在它们采用旧的默认值时消失。

此外,尽量避免将值作为位置参数返回; 在可能的情况下,返回值作为关键字参数以获得最大的未来兼容性。 如果您更频繁地更改事物的名称而不是它们在构造函数参数列表中的位置,您可能更喜欢位置,但请记住,人们将根据序列化版本重建您的字段一段时间(可能是几年),具体取决于多长时间您的迁移为之而生。

您可以通过查看包含该字段的迁移来查看解构的结果,并且您可以通过对字段进行解构和重构来测试单元测试中的解构:

name, path, args, kwargs = my_field_instance.deconstruct()
new_instance = MyField(*args, **kwargs)
self.assertEqual(my_field_instance.some_attribute, new_instance.some_attribute)

更改自定义字段的基类

您无法更改自定义字段的基类,因为 Django 不会检测到更改并为其进行迁移。 例如,如果您从以下内容开始:

class CustomCharField(models.CharField):
    ...

然后决定使用 TextField 代替,您不能像这样更改子类:

class CustomCharField(models.TextField):
    ...

相反,您必须创建一个新的自定义字段类并更新您的模型以引用它:

class CustomCharField(models.CharField):
    ...

class CustomTextField(models.TextField):
    ...

删除字段 中所述,只要您有引用它的迁移,就必须保留原始 CustomCharField 类。


记录您的自定义字段

与往常一样,您应该记录您的字段类型,以便用户知道它是什么。 除了为其提供对开发人员有用的文档字符串外,您还可以允许管理应用程序的用户通过 django.contrib.admindocs 应用程序查看字段类型的简短描述。 为此,请在自定义字段的 description 类属性中提供描述性文本。 在上面的例子中,HandFieldadmindocs 应用程序显示的描述将是“一手牌(桥牌风格)”。

django.contrib.admindocs 显示中,字段描述插入了 field.__dict__,这允许描述包含字段的参数。 例如,CharField 的描述是:

description = _("String (up to %(max_length)s)")

有用的方法

创建 Field 子类后,您可能会考虑覆盖一些标准方法,具体取决于您的字段的行为。 下面的方法列表的重要性大致按降序排列,因此从顶部开始。

自定义数据库类型

假设您创建了一个名为 mytype 的 PostgreSQL 自定义类型。 您可以继承 Field 并实现 db_type() 方法,如下所示:

from django.db import models

class MytypeField(models.Field):
    def db_type(self, connection):
        return 'mytype'

拥有 MytypeField 后,您可以在任何型号中使用它,就像任何其他 Field 类型一样:

class Person(models.Model):
    name = models.CharField(max_length=80)
    something_else = MytypeField()

如果您的目标是构建一个与数据库无关的应用程序,您应该考虑数据库列类型的差异。 例如,PostgreSQL 中的日期/时间列类型称为 timestamp,而 MySQL 中的同一列称为 datetime。 您可以通过检查 connection.vendor 属性在 db_type() 方法中处理此问题。 当前的内置供应商名称为:sqlitepostgresqlmysqloracle

例如:

class MyDateField(models.Field):
    def db_type(self, connection):
        if connection.vendor == 'mysql':
            return 'datetime'
        else:
            return 'timestamp'

db_type()rel_db_type() 方法在框架为您的应用程序构建 CREATE TABLE 语句时被 Django 调用——也就是说,当您第一次创建表时. 在构造包含模型字段的 WHERE 子句时,也会调用这些方法——也就是说,当您使用 get()filter()exclude() 并将模型字段作为参数。 它们不会在任何其他时间被调用,因此它可以执行稍微复杂的代码,例如上面示例中的 connection.settings_dict 检查。

某些数据库列类型接受参数,例如 CHAR(25),其中参数 25 表示最大列长度。 在这种情况下,如果在模型中指定参数而不是在 db_type() 方法中进行硬编码,则会更加灵活。 例如,这里显示的 CharMaxlength25Field 没有多大意义:

# This is a silly example of hard-coded parameters.
class CharMaxlength25Field(models.Field):
    def db_type(self, connection):
        return 'char(25)'

# In the model:
class MyModel(models.Model):
    # ...
    my_field = CharMaxlength25Field()

这样做的更好方法是在运行时指定参数——即,当类被实例化时。 为此,请实现 Field.__init__(),如下所示:

# This is a much more flexible example.
class BetterCharField(models.Field):
    def __init__(self, max_length, *args, **kwargs):
        self.max_length = max_length
        super().__init__(*args, **kwargs)

    def db_type(self, connection):
        return 'char(%s)' % self.max_length

# In the model:
class MyModel(models.Model):
    # ...
    my_field = BetterCharField(25)

最后,如果您的列需要真正复杂的 SQL 设置,请从 db_type() 返回 None。 这将导致 Django 的 SQL 创建代码跳过此字段。 然后,您负责以其他方式在右表中创建列,但这为您提供了一种告诉 Django 的方法。

rel_db_type() 方法由诸如 ForeignKeyOneToOneField 之类的字段调用,这些字段指向另一个字段以确定它们的数据库列数据类型。 例如,如果您有一个 UnsignedAutoField,您还需要指向该字段的外键使用相同的数据类型:

# MySQL unsigned integer (range 0 to 4294967295).
class UnsignedAutoField(models.AutoField):
    def db_type(self, connection):
        return 'integer UNSIGNED AUTO_INCREMENT'

    def rel_db_type(self, connection):
        return 'integer UNSIGNED'

将值转换为 Python 对象

如果您的自定义 Field 类处理比字符串、日期、整数或浮点数更复杂的数据结构,那么您可能需要覆盖 from_db_value()to_python( )

如果字段子类存在,则在从数据库加载数据时的所有情况下都会调用 from_db_value(),包括聚合和 values() 调用。

to_python() 由反序列化调用,并在 clean() 方法期间从表单使用。

作为一般规则,to_python() 应该优雅地处理以下任何参数:

  • 正确类型的实例(例如,我们正在进行的示例中的 Hand)。
  • 一个字符串
  • None(如果字段允许 null=True

在我们的 HandField 类中,我们将数据作为 VARCHAR 字段存储在数据库中,因此我们需要能够处理字符串和 from_db_value() 中的 None。 在 to_python() 中,我们还需要处理 Hand 实例:

import re

from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _

def parse_hand(hand_string):
    """Takes a string of cards and splits into a full hand."""
    p1 = re.compile('.{26}')
    p2 = re.compile('..')
    args = [p2.findall(x) for x in p1.findall(hand_string)]
    if len(args) != 4:
        raise ValidationError(_("Invalid input for a Hand instance"))
    return Hand(*args)

class HandField(models.Field):
    # ...

    def from_db_value(self, value, expression, connection):
        if value is None:
            return value
        return parse_hand(value)

    def to_python(self, value):
        if isinstance(value, Hand):
            return value

        if value is None:
            return value

        return parse_hand(value)

请注意,我们总是从这些方法返回一个 Hand 实例。 这就是我们想要存储在模型属性中的 Python 对象类型。

对于 to_python(),如果在值转换过程中出现任何错误,您应该引发 ValidationError 异常。


将 Python 对象转换为查询值

由于使用数据库需要以两种方式进行转换,如果您覆盖 from_db_value() 您还必须覆盖 get_prep_value() 以将 Python 对象转换回查询值。

例如:

class HandField(models.Field):
    # ...

    def get_prep_value(self, value):
        return ''.join([''.join(l) for l in (value.north,
                value.east, value.south, value.west)])

警告

如果您的自定义字段使用 MySQL 的 CHARVARCHARTEXT 类型,则必须确保 get_prep_value() 始终返回字符串类型。 当对这些类型执行查询并且提供的值为整数时,MySQL 执行灵活且意外的匹配,这可能导致查询在其结果中包含意外的对象。 如果始终从 get_prep_value() 返回字符串类型,则不会发生此问题。


将查询值转换为数据库值

某些数据类型(例如,日期)需要采用特定格式才能被数据库后端使用。 get_db_prep_value() 是应该进行这些转换的方法。 将用于查询的特定连接作为 connection 参数传递。 这允许您在需要时使用后端特定的转换逻辑。

例如,Django 对其 BinaryField 使用以下方法:

def get_db_prep_value(self, value, connection, prepared=False):
    value = super().get_db_prep_value(value, connection, prepared)
    if value is not None:
        return connection.Database.Binary(value)
    return value

如果您的自定义字段在保存时需要与用于普通查询参数的转换不同的特殊转换,您可以覆盖 get_db_prep_save()


保存前预处理值

如果您想在保存之前对值进行预处理,可以使用 pre_save()。 比如django的DateTimeField就是用这个方法在auto_now或者auto_now_add的情况下正确设置属性。

如果确实覆盖了此方法,则必须在最后返回该属性的值。 如果对值进行任何更改,您还应该更新模型的属性,以便保存对模型的引用的代码将始终看到正确的值。


为模型字段指定表单字段

要自定义 ModelForm 使用的表单字段,您可以覆盖 formfield()

表单字段类可以通过 form_classchoices_form_class 参数指定; 如果字段指定了选项,则使用后者,否则使用前者。 如果未提供这些参数,则将使用 CharFieldTypedChoiceField

所有 kwargs 字典都直接传递给表单字段的 __init__() 方法。 通常,您需要做的就是为 form_class(可能还有 choices_form_class)参数设置一个好的默认值,然后将进一步的处理委托给父类。 这可能需要您编写自定义表单字段(甚至是表单小部件)。 有关这方面的信息,请参阅 表单文档

继续我们正在进行的示例,我们可以将 formfield() 方法编写为:

class HandField(models.Field):
    # ...

    def formfield(self, **kwargs):
        # This is a fairly standard way to set up some defaults
        # while letting the caller override them.
        defaults = {'form_class': MyFormField}
        defaults.update(kwargs)
        return super().formfield(**defaults)

这假设我们已经导入了一个 MyFormField 字段类(它有自己的默认小部件)。 本文档不涵盖编写自定义表单字段的详细信息。


模拟内置字段类型

如果你已经创建了一个 db_type() 方法,你就不用担心 get_internal_type() - 它不会被使用太多。 但有时,您的数据库存储在类型上与某些其他字段相似,因此您可以使用该其他字段的逻辑来创建正确的列。

例如:

class HandField(models.Field):
    # ...

    def get_internal_type(self):
        return 'CharField'

无论我们使用哪个数据库后端,这都意味着 :djadmin:`migrate` 和其他 SQL 命令创建正确的列类型来存储字符串。

如果 get_internal_type() 为您正在使用的数据库后端返回一个 Django 不知道的字符串——也就是说,它没有出现在 django.db.backends.<db_name>.base.DatabaseWrapper.data_types 中——该字符串仍将由序列化程序,但默认的 db_type() 方法将返回 None。 有关为什么这可能有用的原因,请参阅 db_type() 的文档。 如果您打算在 Django 之外的其他地方使用序列化程序输出,那么将描述性字符串作为序列化程序的字段类型是一个有用的想法。


转换字段数据以进行序列化

要自定义序列化程序如何序列化值,您可以覆盖 value_to_string()。 使用 value_from_object() 是在序列化之前获取字段值的最佳方法。 例如,由于 HandField 无论如何都使用字符串作为其数据存储,我们可以重用一些现有的转换代码:

class HandField(models.Field):
    # ...

    def value_to_string(self, obj):
        value = self.value_from_object(obj)
        return self.get_prep_value(value)

一些一般性建议

编写自定义字段可能是一个棘手的过程,尤其是在 Python 类型与数据库和序列化格式之间进行复杂转换时。 这里有一些技巧可以让事情进展得更顺利:

  1. 查看现有的 Django 字段(在 django/db/models/fields/__init__.py 中)以获得灵感。 尝试找到一个与您想要的字段相似的字段并对其进行一点扩展,而不是从头开始创建一个全新的字段。
  2. __str__() 方法放在您作为字段包装的类上。 有很多地方字段代码的默认行为是在值上调用 str()。 (在本文档的示例中,value 将是 Hand 实例,而不是 HandField)。 所以如果你的 __str__() 方法自动转换为你的 Python 对象的字符串形式,你可以省去很多工作。


编写 FileField 子类

除了上述方法之外,处理文件的字段还有一些其他特殊要求必须考虑在内。 FileField 提供的大部分机制,例如控制数据库存储和检索,可以保持不变,留下子类来应对支持特定类型文件的挑战。

Django 提供了一个 File 类,用作文件内容和操作的代理。 可以将其子类化以自定义访问文件的方式以及可用的方法。 它位于 django.db.models.fields.files,其默认行为在 文件文档 中有解释。

一旦创建了 File 的子类,必须告知新的 FileField 子类使用它。 为此,请将新的 File 子类分配给 FileField 子类的特殊 attr_class 属性。

一些建议

除了上述细节之外,还有一些准则可以大大提高字段代码的效率和可读性。

  1. Django 自己的 ImageField(在 django/db/models/fields/files.py 中)的源代码是一个很好的示例,说明如何将 FileField 子类化以支持特定类型的文件,因为它包含了所描述的所有技术以上。
  2. 尽可能缓存文件属性。 由于文件可能存储在远程存储系统中,因此检索它们可能会花费额外的时间,甚至金钱,这并不总是必要的。 一旦检索文件以获取有关其内容的一些数据,请尽可能多地缓存该数据,以减少在后续调用该信息时必须检索该文件的次数。