编写自定义模型字段(model fields)

介绍

字段参考 文档介绍了如何使用 Django 的标准字段类—— CharFieldDateField,等等。大多数情况下,这些类就是你需要的。虽然有时候,Django 的版本不能精确地匹配你的需求,或者你想使用的字段与 Django 内置的完全不同。

Django 内置的字段类型并未覆盖所有可能的数据库字段类型——只有常见的类型,例如 VARCHARINTEGER。对于更多模糊的列类型,例如地理多边形(geographic polygons),甚至是用户创建的类型,例如 PostgreSQL custom types,你可以自定义 Django 的 Field 子类。

或者,你有一个复杂的 Python 对象,它可以以某种形式序列化,适应标准的数据库列类型。这是另一个 Field 子类能帮助你配合模型使用你的对象的示例。

我们的示例对象

创建自定义字段要求注意一些细节。为了简化问题,我们在本文档中全程使用同一实例:封装一个 Python 对象,代表手上 桥牌 的细节。不要担心,你不需要知道如何玩桥牌就能学习此例子。你只需知道 52 张牌被均分给 4 个玩家,一般称他们 西。我们的类长这样:

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

This is an ordinary Python class, with nothing Django-specific about it. We'd like to be able to do things like this in our models (we assume the hand attribute on the model is an instance of Hand):

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

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

对模型中的 hand 属性的赋值与取值操作与其它 Python 类一直。技巧是告诉 Django 如何保存和加载对象。

为了在模型中使用 Hand 类,我们 需要修改这个类。这很不错,因为这以为着你仅需为已存在的类编写模型支持,即便你不能修改源码。

注解

你可能只想要自定义数据库列的优点,并在模型中像使用标准 Python 那样;字符串,或浮点数,等等。这种情况与 Hand 例子类似,在进行过程中,我们将注意到差异。

背后的理论

数据库存储

Let's start with model fields. If you break it down, a model field provides a way to take a normal Python object -- string, boolean, datetime, or something more complex like Hand -- and convert it to and from a format that is useful when dealing with the database. (Such a format is also useful for serialization, but as we'll see later, that is easier once you have the database side under control).

模型中的字段必须能以某种方式转换为已存在的数据库列类型。不能的数据库提供不同的可用列类型集,但规则仍相同:你只需要处理这些类型。你想存在数据库中的任何数据都必须能适配这些类型中的某一个。

Normally, you're either writing a Django field to match a particular database column type, or you will need a way to convert your data to, say, a string.

对于我们的 Hand 示例,我们能将卡片数据转换为一个 104 个字符的字符串,通过以预定义的顺序连接所有卡片——也就是说,先连接 所拥有的卡,随后是 ,和 西。所有 Hand 对象能被保存在数据库中的文本或字符列中。

一个字段(Field)类做了什么?

所有的 Django 字段(本页提到的 字段 均指模型字段,而不是 表单字段)都是 django.db.models.Field 的子类。对于所有字段,Django 记录的大部分信息是一样的——名字,帮助文本,是否唯一,等等。存储行为由 Field 处理。稍后,我们会深入了解 Field 能做什么;现在, 可以说万物源于 Field,并在其基础上自定义了类的关键行为。

了解 Django 字段类不保存在模型属性中很重要。模型属性包含普通的 Python 对象。你所以定义的字段类实际上在模型类创建时在 Meta 类中(这是如何实现的在这里不重要)。这是因为在仅创建和修改属性时,字段类不是必须的。相反,他们提供了属性值间转换的机制,并决定了什么被存入数据库或发送给 序列化器

在你创建自定义字段时牢记这点。你所写的 Django 的 Field 子类提供了多种在 Python 实例和数据库/序列化器之间的转换机制(比如,保存值和使用值进行查询之间是不同的)。听起来有点迷糊,但别担心——通过以下的例子会清晰起来。只要记住,在你需要一个自定义字段时,只需创建两个类:

  • 第一个类是用户需要操作的 Python 对象。它们会复制给模型属性,它们会为了显示而读取属性,就想这样。这里本例中的 Hand 类。
  • 第二类是 Field 的子类。这个类知道如何在永久存储格式和 Python 格式之间来回转换。

编写一个 field 子类

计划编写第一个 Field 子类时,需要先想想新字段和哪个已有的 Field 最相似。你会继承 Django 字段节约你的时间吗?如果不会,你需要继承 Field 类,从它继承了一切。

初始化新字段有点麻烦,因为要从公共参数中分离你需要的参数,并将剩下的传给父类 Field__init__() 方法(或你的父类)。

在本例中,我们会调用 HandField。(调用你的 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 个字符。

注解

Many of Django's model fields accept options that they don't do anything with. For example, you can pass both editable and auto_now to a django.db.models.DateField and it will ignore the editable parameter (auto_now being set implies editable=False). No error is raised in this case.

This behavior simplifies the field classes, because they don't need to check for options that aren't necessary. They pass all the options to the parent class and then don't use them later on. It's up to you whether you want your fields to be more strict about the options they select, or to use the more permissive behavior of the current fields.

Field.__init__() 方法接收以下参数:

上述列表中所有无解释的选项与在普通 Django 字段中的作用一样。参见 字段文档 获取例子和细节信息。

字段解析

与编写 __init__() 方法相对是编写 deconstruct() 方法。它在 模型迁移 期间告诉 Django 如何获取你的新字段的一个实例,并将其转为序列化形式——特别是,传递什么参数给 __init__() 来重新创建它。

如果你未在继承的字段之前添加任何选项,就不需要编写新的 deconstruct() 方法。然而,如果你正在修改传递给 __init__() 的参数(像 HandField 中的一样),你需要增补被传递的值。

deconstruct() returns a tuple of four items: the field's attribute name, the full import path of the field class, the positional arguments (as a list), and the keyword arguments (as a dict). Note this is different from the deconstruct() method for custom classes which returns a tuple of three things.

作为自定义字段的作者,你不需要担心前两个值;基类 Field 已包含处理字段属性名和导入路径的代码。然后,你仍必须关注位置参数和关键字参数,这些是你最有可能改的东西。

例如,在 HandField 类中,我们总是强制设置 __init__() 的长度。基类 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

更多的复杂例子超出本文的范围,但是请牢记——对于你的字段实例的任意配置,deconstruct() 必须返回能传递给 __init__ 的参数重构状态。

如果你在父类 Field 中设置了新的默认值需要额外注意;说明你希望总是包含它们,而不是在它们采用旧有值时消失。

In addition, try to avoid returning values as positional arguments; where possible, return values as keyword arguments for maximum future compatibility. If you change the names of things more often than their position in the constructor's argument list, you might prefer positional, but bear in mind that people will be reconstructing your field from the serialized version for quite a while (possibly years), depending how long your migrations live for.

You can see the results of deconstruction by looking in migrations that include the field, and you can test deconstruction in unit tests by deconstructing and reconstructing the 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 类只要你还有迁移指向它。

为自定义字段编写文档

As always, you should document your field type, so users will know what it is. In addition to providing a docstring for it, which is useful for developers, you can also allow users of the admin app to see a short description of the field type via the django.contrib.admindocs application. To do this provide descriptive text in a description class attribute of your custom field. In the above example, the description displayed by the admindocs application for a HandField will be 'A hand of cards (bridge style)'.

django.contrib.admindocs 展示的内容中,字段描述在 field.__dict__ 中差值,它允许描述包含字段参数。例如, CharField 的说明是:

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

实用方法

一旦你已创建了 Field 的子类,你可能会考虑重写一些标准方法,这取决于你的字段行为。以下列表中的方法大致按重要性降序排列,即从上至下。

自定义数据库类型

假设你已创建了一个 PostgreSQL 自定义字段,名叫 mytype。你可以继承 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()

If you aim to build a database-agnostic application, you should account for differences in database column types. For example, the date/time column type in PostgreSQL is called timestamp, while the same column in MySQL is called datetime. You can handle this in a db_type() method by checking the connection.settings_dict['ENGINE'] attribute.

例子:

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

db_type()rel_db_type() 方法由 Django 框架在为应用构建 CREATE TABLE 语句时调用——即你第一次创建数据表的时候。这些方法也在构建一个包含此模型字段的 WHERE 字句时调用——即你在利用 QuerySet 方法(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()

The better way of doing this would be to make the parameter specifiable at run time -- i.e., when the class is instantiated. To do that, implement Field.__init__(), like so:

# 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)

Finally, if your column requires truly complex SQL setup, return None from db_type(). This will cause Django's SQL creation code to skip over this field. You are then responsible for creating the column in the right table in some other way, but this gives you a way to tell Django to get out of the way.

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 转为查询值

Since using a database requires conversion in both ways, if you override from_db_value() you also have to override get_prep_value() to convert Python objects back to query values.

例子:

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() 返回字符串类型的时候出现。

将查询值转为数据库值

某些数据类型(比如 dates)在数据库后端处理前要转为某种特定格式。 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 的 DateTimeFieldauto_nowauto_now_add 中利用此方法正确设置属性。

如果你重写了此方法,你必须在最后返回该属性的值。如果修改了值,那么你也需要更新模型属性,这样持有该引用的模型总会看到正确的值。

为模型字段指定表单字段

为了自定义 ModelForm 使用的表单属性,你必须重写 formfield()

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

完整的 kwargs 被直接传递给表单字段的 __init__() 方法。一般的,你要做的全部工作就是为 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'

无论我们使用了哪个数据库后端, migrate 或其它 SQL 命令总会在保存字符串时为其创建正确的列类型。

get_internal_type() 返回了当前数据库后端(即 django.db.backends.<db_name>.base.DatabaseWrapper.data_types 中未出现的后端)无法理解的字符串——该字符串仍会被序列化器使用的,但是默认的 db_type() 方法会返回 None。查阅文档 db_type() 了解为啥有用。如果您打算在 Django 之外的其他地方使用序列化器输出,那么将描述性字符串作为序列化器的字段类型是一个有用的想法。

为序列化转换字段数据

自定义序列化器序列化值的流程,你要重写 value_to_string()。使用 value_to_string() 是在序列化之前获取字段值的最佳方法。举个例子,由于 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 文档 中介绍。

Once a subclass of File is created, the new FileField subclass must be told to use it. To do so, assign the new File subclass to the special attr_class attribute of the FileField subclass.

一些建议

除了上述细节,下面还有一些准则,有助于极大地提高字段代码的效率和可读性。

  1. Django 的 ImageField 的源码(位于 django/db/models/fields/files.py)就是个展示如何继承 FileField 支持特定文件的不错例子,因为它包含了上述所有技巧。
  2. 尽可能的缓存文件属性。因为文件可能保存在远端存储系统中,检出它们会消耗额外的时间,甚至是钱,且不总是必要的。一旦检出某个文件,获取其内容,尽可能缓存所有数据,以减少后续调用再次检索文件的次数。
Back to Top