编写自定义模型字段

引言¶ T0>

model reference文档解释了如何使用Django的标准字段类 - CharFieldDateField等等。出于多种目的,需要。 但是,有时Django版本不能满足您的具体要求,或者您需要使用与Django一起提供的完全不同的字段。

Django的内置字段类型不涵盖每种可能的数据库列类型 - 只包括常见类型,如VARCHARINTEGER For more obscure column types, such as geographic polygons or even user-created types such as PostgreSQL custom types, you can define your own Django Field subclasses.

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

我们的示例对象

创建自定义字段需要注意一些细节。 为了使事情更易于理解,我们将在整个文档中使用一个一致的示例:在Bridge中包装一个表示卡交易的Python对象。 不要担心,你不必知道如何玩Bridge来跟随这个例子。 你只需要知道52张牌被平均分配给四名玩家,他们通常被称为西 T3>。 我们的班级看起来像这样:

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例子类似,我们将会注意到任何差异。

背景理论

数据库存储

The simplest way to think of a model field is that it 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 (and serialization, but, as we’ll see later, that falls out fairly naturally once you have the database side under control).

必须以某种方式将模型中的字段转换为适合现有数据库列类型。 不同的数据库提供了不同的有效列类型集合,但是规则仍然是一样的:这些是​​唯一需要使用的类型。 你想存储在数据库中的任何东西都必须适合其中的一种类型。

通常情况下,你要么写一个Django字段来匹配一个特定的数据库列类型,要么有一个相当直接的方法来将你的数据转换成一个字符串。

对于我们的Hand例子,我们可以将卡片数据转换成一个由104个字符组成的字符串,将所有的卡片以预先确定的顺序连接在一起 - 比如说,所有的north卡片第一,然后是西卡。 So Hand objects can be saved to text or character columns in the database.

字段类是做什么的?

Django的所有字段(以及本文中字段,我们始终指模型字段而不是form fields)是django.db.models.Field Django关于字段记录的大部分信息对于所有字段都是通用的 - 名称,帮助文本,唯一性等等。 存储所有这些信息由Field处理。 我们将详细介绍Field可以稍后做些什么;现在,足以说所有东西都从Field下降,然后定制类行为的关键部分。

意识到Django字段类不是存储在模型属性中的是非常重要的。 模型属性包含普通的Python对象。 您在模型中定义的字段类实际上是在创建模型类时存储在Meta类中的(关于如何完成此操作的确切细节在这里并不重要)。 这是因为在创建和修改属性时,字段类是不必要的。 相反,它们提供了在属性值和存储在数据库中或者发送到serializer之间转换的机制。

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

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

编写一个字段子类

When planning your Field subclass, first give some thought to which existing Field class your new field is most similar to. 你可以继承现有的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字段具有相同的含义。 有关示例和详细信息,请参阅field documentation

字段解构

写入__init__()方法的对应点是写入deconstruct()方法。 这个方法告诉Django如何获取新字段的一个实例,并将其减少到一个序列化的形式,特别是传递给__init__()的参数来重新创建它。

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

deconstruct()的合约很简单;它返回一个包含四个元素的元组:字段的属性名称,字段类的完整导入路径,位置参数(作为列表)和关键字参数(作为字典)。 请注意,这与自定义类的deconstruct()方法for custom classes

作为一个自定义字段的作者,你不需要关心前两个值;基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

如果你添加一个新的关键字参数,你需要编写代码把它的值自己放到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

More complex examples are beyond the scope of this document, but remember - for any configuration of your Field instance, deconstruct() must return arguments that you can pass to __init__ to reconstruct that state.

如果在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):
    ...

正如在removing fields中所讨论的那样,只要您有引用它的迁移,就必须保留原始CustomCharField类。

记录您的自定义字段

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

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'

The db_type() and rel_db_type() methods are called by Django when the framework constructs the CREATE TABLE statements for your application – that is, when you first create your tables. 在构造包含模型字段的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()

这样做的更好的方法是使参数在运行时可以被指定,即当类被实例化时。 为此,只需实现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()

If present for the field subclass, from_db_value() will be called in all circumstances when the data is loaded from the database, including in aggregates and values() calls.

to_python() is called by deserialization and during the clean() method used from forms.

As a general rule, to_python() should deal gracefully with any of the following arguments:

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

In our HandField class, we’re storing the data as a VARCHAR field in the database, so we need to be able to process strings and None in the from_db_value(). 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 to_python() 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()返回字符串类型,则不会出现此问题。

将查询值转换为数据库值

某些数据类型(例如日期)需要使用特定格式才能被数据库后端使用。 get_db_prep_value() is the method where those conversions should be made. 将用于查询的特定连接作为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() 例如,在auto_nowauto_now_add的情况下,Django的DateTimeField使用此方法正确设置属性。

如果您重写此方法,则必须在最后返回该属性的值。 如果对该值进行任何更改,则还应该更新模型的属性,以便代码保持对模型的引用将始终显示正确的值。

指定模型字段的表单字段

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

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

所有的kwargs字典直接传递给表单字段的__init__()方法。 通常,你所要做的就是为form_class(也许是choices_form_class)参数设置一个很好的默认值,然后把进一步的处理委托给父类。 这可能需要你写一个自定义的表单域(甚至是一个表单控件)。 有关此信息,请参阅forms documentation

继续我们正在进行的例子,我们可以将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字段类(它有自己的默认小部件)。 本文档不包括编写自定义表单域的详细信息。

模拟内置字段类型

If you have created a db_type() method, you don’t need to worry about get_internal_type() – it won’t be used much. 但有时候,数据库存储的类型与其他字段类似,所以可以使用其他字段的逻辑来创建正确的列。

例如:

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以外的其他地方使用序列化程序输出。

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

要自定义串行器如何串行化值,可以覆盖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子类

除上述方法之外,处理文件的字段还有一些其他的特殊要求,这些要求必须考虑在内。 The majority of the mechanics provided by FileField, such as controlling database storage and retrieval, can remain unchanged, leaving subclasses to deal with the challenge of supporting a particular type of file.

Django提供了一个File类,该类用作文件内容和操作的代理。 这可以被分类以定制如何访问文件,以及可用的方法。 它位于django.db.models.fields.files中,其默认行为在file documentation中进行了解释。

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

一些建议

除上述细节外,还有一些指导方针可以大大提高字段代码的效率和可读性。

  1. Django自己的ImageField的源代码(位于django/db/models/fields/files.py)是如何继承FileField以支持特定类型的文件,因为它结合了上述所有技术。
  2. 尽可能缓存文件属性。 由于文件可能存储在远程存储系统中,检索它们可能会花费额外的时间,甚至是花钱,而这并不总是必要的。 一旦文件被检索以获得关于其内容的一些数据,就尽可能多地缓存该数据,以减少在随后调用该信息时必须检索该文件的次数。