Skip to main content

编写自定义模型字段

介绍

模型参考 文档解释了如何使用Django的标准字段类–CharFieldDateField 等。对于许多目的,这些类都是你需要的。有时,Django版本不会满足您的精确要求,或者您想使用与Django附带的完全不同的字段。

Django的内置字段类型不覆盖每个可能的数据库列类型 - 只包括常见类型,如 VARCHARINTEGER。对于更加模糊的列类型(例如地理多边形或甚至用户创建的类型(如 PostgreSQL custom types)),您可以定义自己的Django Field 子类。

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

我们的示例对象

创建自定义字段需要注意细节。为了使事情更容易遵循,我们将在本文中使用一个一致的例子:将代表卡交易的Python对象包含在 Bridge 的手中。不要担心,你不必知道如何玩Bridge以下这个例子。你只需要知道52张卡平分给四个玩家,他们传统上称为 northeastsouthwest。我们的类看起来像这样:

class Hand(object):
    """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 的子类。 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(HandField, self).__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(HandField, self).__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(HandField, self).deconstruct()
        del kwargs["max_length"]
        return name, path, args, 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(CommaSepField, self).__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super(CommaSepField, self).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 类,只要您具有引用它的迁移。

记录自定义字段

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

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。在 db_type() 方法中处理这个问题的最简单的方法是检查 connection.settings_dict['ENGINE'] 属性。

例如:

class MyDateField(models.Field):
    def db_type(self, connection):
        if connection.settings_dict['ENGINE'] == 'django.db.backends.mysql':
            return 'datetime'
        else:
            return 'timestamp'

当框架为您的应用程序构建 CREATE TABLE 语句时,即首次创建表时,将由Django调用 db_type()rel_db_type() 方法。当构造包含模型字段的 WHERE 子句时,也就是当使用诸如 get()filter()exclude() 的QuerySet方法检索数据并将模型字段作为参数时,也会调用这些方法。它们在任何其他时间都不被调用,因此它可以执行稍微复杂的代码,例如上面示例中的 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(BetterCharField, self).__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'
New in Django 1.10:

添加 rel_db_type() 方法。

将值转换为Python对象

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

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

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 ugettext_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, context):
        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对象转换为查询值

由于使用数据库需要以两种方式进行转换,如果您覆盖 to_python(),还必须覆盖 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)])

警告

如果您的自定义字段使用 CHARVARCHARTEXT 类型的MySQL,您必须确保 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(BinaryField, self).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的 DateTimeFieldauto_nowauto_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(HandField, self).formfield(**defaults)

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

模拟内置字段类型

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

例如:

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

    def get_internal_type(self):
        return 'CharField'

不管我们使用哪个数据库后端,这将意味着 migrate 和其他SQL命令创建用于存储字符串的正确的列类型。

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

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

要自定义值如何由序列化程序序列化,您可以覆盖 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__() (在Python 2上的 __unicode__())方法放在您作为字段打包的类上。有很多地方,其中字段代码的默认行为是调用 force_text() 的值。 (在本文档的示例中,value 将是 Hand 实例,而不是 HandField)。因此,如果您的 __str__() 方法(Python 2上的 __unicode__())自动转换为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. 尽可能缓存文件属性。由于文件可以存储在远程存储系统中,因此检索它们可能花费额外的时间或甚至金钱,这并不总是必需的。一旦检索到文件以获得关于其内容的一些数据,就尽可能地缓存那些数据,以减少在该信息的后续调用中必须检索文件的次数。