如何创建自定义模型字段

简介

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

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

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

我们的示例对象

创建自定义字段需要一点细致的考虑。为了使事情更容易理解,我们将在本文档中始终使用一个一致的示例:将表示一手桥牌牌局的 Python 对象包装起来。桥牌。不用担心,你不需要知道如何玩桥牌就能理解这个例子。你只需要知道 52 张牌平均分发给四位玩家,他们传统上被称为北、东、南和西。我们的类看起来像这样

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 个字符的字符串 - 例如,首先是所有北方的牌,然后是东、南和西方的牌。因此,Hand 对象可以保存到数据库中的文本或字符列中。

字段类有什么作用?

所有 Django 的字段(当我们在本文档中说字段时,我们始终是指模型字段,而不是表单字段)都是 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().__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() 返回一个包含四个项目的元组:字段的属性名称、字段类的完整导入路径、位置参数(作为列表)和关键字参数(作为字典)。请注意,这与 自定义类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)

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

你可以重写 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): ...

删除字段 中所述,只要你拥有引用它的迁移,就必须保留原始的 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。您可以通过检查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子句时被调用——也就是说,当您使用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设置,请从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_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().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_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. 尽可能缓存文件属性。由于文件可能存储在远程存储系统中,因此检索它们可能会花费额外的时间,甚至金钱,而这些时间和金钱并不总是必要的。一旦检索到文件以获取有关其内容的一些数据,请尽可能缓存尽可能多的数据,以减少必须在后续调用中检索文件的次数。

返回顶部