如何创建自定义模型字段

介绍

这个 model reference 文档解释了如何使用Django的标准字段类-- CharFieldDateField 等等,对于许多目的来说,这些类就是您所需要的。不过,有时django版本无法满足您的精确要求,或者您希望使用与django附带的字段完全不同的字段。

Django的内置字段类型不包括所有可能的数据库列类型——只包括常见的类型,例如 VARCHARINTEGER . 对于更模糊的列类型,例如地理多边形,甚至用户创建的类型,例如 PostgreSQL custom types ,您可以定义自己的django Field 子类。

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

我们的示例对象

创建自定义字段需要注意细节。为了使事情更容易理解,我们将在整个文档中使用一个一致的示例:将一个代表卡片交易的python对象包装在 Bridge. 别担心,你不必知道如何打桥牌来遵循这个例子。你只需要知道52张牌是平均分配给4个玩家的,他们通常被称为 east南方west . 我们班看起来像这样:

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

我们分配并从 hand 属性在我们的模型中与其他任何Python类一样。诀窍是告诉Django如何处理保存和加载这样的对象。

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

备注

您可能只希望利用自定义数据库列类型,并将数据作为模型中的标准python类型来处理;例如字符串或浮点数。这个案子和我们的案子很相似 Hand 举例来说,我们将在进行过程中注意到任何差异。

背景理论

数据库存储

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

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

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

为我们的 Hand 例如,我们可以将卡数据转换为104个字符的字符串,方法是按照预定的顺序将所有卡连接在一起--也就是说,将所有 north 首先是纸牌,然后是 eastsouthwest 扑克牌。所以 Hand 可以将对象保存到数据库中的文本或字符列。

外勤课做什么?

所有Django的领域(当我们说 领域 在本文档中,我们总是指模型字段,而不是 form fields )是的子类 django.db.models.Field . Django记录的关于字段的大多数信息对于所有字段都是通用的——名称、帮助文本、唯一性等等。存储所有处理的信息 Field . 我们会详细了解 Field 以后可以做;现在,只要说一切都是从 Field 然后定制类行为的关键部分。

重要的是要认识到django字段类不是存储在模型属性中的类。模型属性包含普通的python对象。在模型中定义的字段类实际上存储在 Meta 创建模型类时的类(如何完成此操作的精确细节在此处不重要)。这是因为在创建和修改属性时不需要字段类。相反,它们提供了在属性值和存储在数据库中或发送到 serializer .

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

  • 第一个类是用户将要操作的python对象。他们将把它分配给模型属性,他们将从中读取以显示目的,类似的事情。这就是 Hand 我们的例子中的类。

  • 第二类是 Field 子类。这个类知道如何在第一个类的永久存储窗体和Python窗体之间来回转换。

编写字段子类

当你计划 Field 子类,首先考虑一下 Field 类您的新字段与最相似。你能把现有的django字段子类化并保存一些工作吗?如果不是,则应将 Field 类,从中派生出所有内容。

初始化新字段是将特定于您的案例的任何参数与常见参数分开,并将后者传递给 __init__() 方法 Field (或您的父类)。

在我们的示例中,我们将调用我们的字段 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 到A django.db.models.DateField 它会忽略 editable 参数 (auto_now 被设定意味着 editable=False )在这种情况下不会出现错误。

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

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

上面列表中没有解释的所有选项都具有与普通django字段相同的含义。见 field documentation 举例说明。

场解构

与你写下你的 __init__() 方法正在将 deconstruct() 方法。它被用在 model migrations 告诉Django如何获取新字段的实例并将其简化为序列化形式-特别是传递给哪些参数 __init__() 来重新创造它。

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

deconstruct() 返回包含四项的元组:字段的属性名、字段类的完整导入路径、位置参数(以列表形式)和关键字参数(作为dict)。注意这与 deconstruct() 方法 for custom classes 它返回三个元素的元组。

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

例如,在我们的 HandField 我们总是强制设置最大长度 __init__() . 这个 deconstruct() 基础上的方法 Field 类将看到这个,并尝试在关键字参数中返回它;因此,为了可读性,我们可以从关键字参数中删除它:

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 超类;您希望确保它们总是包含在其中,而不是在它们采用旧的默认值时消失。

此外,尽量避免将值作为位置参数返回;如果可能,返回值作为关键字参数,以获得最大的未来兼容性。如果更改事物的名称比更改它们在构造函数的参数列表中的位置更频繁,那么您可能更倾向于使用位置,但请记住,人们将在相当长的时间内(可能数年)从序列化版本中重建字段,这取决于迁移的生存期。

您可以通过查看包含字段的迁移来查看解构的结果,还可以通过解构和重构字段在单元测试中测试解构:

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

不影响数据库列定义的字段属性

您可以覆盖 Field.non_db_attrs 若要自定义不影响列定义的字段属性,请执行以下操作。在模型迁移过程中使用它来检测无操作 AlterField 行动。

例如::

class CommaSepField(models.Field):
    @property
    def non_db_attrs(self):
        return super().non_db_attrs + ("separator",)

更改自定义字段的基类

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

class CustomCharField(models.CharField):
    ...

然后决定要使用 TextField 相反,您不能像这样更改子类:

class CustomCharField(models.TextField):
    ...

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

class CustomCharField(models.CharField):
    ...


class CustomTextField(models.TextField):
    ...

正如在 removing fields ,必须保留原件 CustomCharField 类,只要有引用它的迁移。

记录自定义字段

和往常一样,您应该记录字段类型,以便用户知道它是什么。除了为其提供docstring(这对开发人员很有用),您还可以允许管理应用程序的用户通过 django.contrib.admindocs 应用程序。为此,请在 description 自定义字段的类属性。在上面的示例中,由 admindocs 申请 HandField 将是“一手牌(桥牌式)”。

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

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

例如::

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

这个 db_type()rel_db_type() Django在框架构造 CREATE TABLE 语句用于您的应用程序--即在您第一次创建表时。这些方法在构造 WHERE 子句,即当您使用QuerySet方法检索数据时,如 get()filter() ,以及 exclude() 并将模型字段作为参数。

某些数据库列类型接受参数,例如 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设置,请返回 Nonedb_type() . 这将导致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 字段,因此我们需要能够处理字符串和 Nonefrom_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对象转换为查询值

因为使用数据库需要以两种方式进行转换,如果重写 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)]
        )

警告

如果自定义字段使用 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().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_nowauto_now_add .

如果确实要重写此方法,则必须在末尾返回属性的值。如果对值进行了任何更改,也应该更新模型的属性,这样保存对模型引用的代码将始终看到正确的值。

为模型字段指定窗体字段

自定义 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 字段类(它有自己的默认小部件)。此文档不包括编写自定义表单域的详细信息。

模拟内置字段类型

如果您创建了 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之外的其他地方使用序列化程序输出,将描述性字符串作为序列化程序字段的类型是一个很有用的想法。

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

要自定义序列化程序序列化值的方式,可以重写 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 documentation .

曾经是 File 是新的 FileField 必须告诉子类使用它。为此,请指定新的 File 特殊的子类 attr_class 的属性 FileField 子类。

一些建议

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

  1. 姜戈自己的消息来源 ImageField (在 django/db/models/fields/files.py )是如何子类化的一个很好的例子 FileField 以支持特定类型的文件,因为它结合了上述所有技术。

  2. 尽可能缓存文件属性。由于文件可能存储在远程存储系统中,因此检索它们可能需要额外的时间,甚至金钱,这并非总是必要的。一旦检索到一个文件以获取关于其内容的一些数据,就尽可能缓存这些数据,以减少在随后调用该信息时必须检索该文件的次数。