编写自定义模型字段 — Django 文档
编写自定义模型字段(model fields)
介绍
模型参考 文档解释了如何使用 Django 的标准字段类 - CharField、DateField 等。 出于许多目的,这些类就是您所需要的。 但是,有时 Django 版本无法满足您的确切要求,或者您希望使用与 Django 附带的字段完全不同的字段。
Django 的内置字段类型并未涵盖所有可能的数据库列类型——仅涵盖常见类型,例如 VARCHAR
和 INTEGER
。 对于更模糊的列类型,例如地理多边形,甚至是用户创建的类型,例如 PostgreSQL 自定义类型 ,您可以定义自己的 Django Field
子类。
或者,您可能有一个复杂的 Python 对象,它可以以某种方式序列化以适应标准数据库列类型。 这是 Field
子类将帮助您将对象与模型一起使用的另一种情况。
我们的示例对象
创建自定义字段需要注意细节。 为了让事情更容易理解,我们将在整个文档中使用一个一致的例子:在 Bridge 的一手牌中包装一个表示发牌的 Python 对象。 不用担心,您不必知道如何玩桥牌即可遵循此示例。 你只需要知道 52 张牌平均分配给四名玩家,他们传统上称为 north、east、south 和 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()
就像任何其他 Python 类一样,我们对模型中的 hand
属性进行赋值和检索。 诀窍是告诉 Django 如何处理保存和加载这样的对象。
为了在我们的模型中使用 Hand
类,我们 根本不需要 更改这个类。 这是理想的,因为这意味着您可以轻松地为无法更改源代码的现有类编写模型支持。
笔记
您可能只想利用自定义数据库列类型并将数据作为模型中的标准 Python 类型处理; 例如,字符串或浮点数。 这个案例类似于我们的 Hand
示例,我们会在进行过程中注意任何差异。
背后的理论
数据库存储
考虑模型字段的最简单方法是它提供了一种方法来获取普通 Python 对象——字符串、布尔值、datetime
或更复杂的东西,如 Hand
——并将其转换为和来自处理数据库时有用的格式(和序列化,但是,正如我们稍后将看到的,一旦您控制了数据库端,它就会很自然地消失)。
模型中的字段必须以某种方式转换以适应现有的数据库列类型。 不同的数据库提供不同的有效列类型集,但规则仍然相同:这些是您必须使用的唯一类型。 您要存储在数据库中的任何内容都必须适合其中一种类型。
通常,您要么编写 Django 字段以匹配特定的数据库列类型,要么使用一种相当简单的方法将数据转换为字符串。
对于我们的 Hand
示例,我们可以通过按预定顺序将所有卡片连接在一起,将卡片数据转换为 104 个字符的字符串 - 例如,首先是所有 north 卡片,然后east、south 和 west 卡。 因此 Hand
对象可以保存到数据库中的文本或字符列中。
一个字段(Field)类做了什么?
Django 的所有字段(当我们在本文档中说 fields 时,我们总是指模型字段而不是 表单字段 )都是 django.db.models.Field[ X176X]。 Django 记录的关于字段的大部分信息对所有字段都是通用的——名称、帮助文本、唯一性等等。 存储所有这些信息由 Field
处理。 我们稍后会详细介绍 Field
可以做什么; 现在,可以说一切都来自 Field
,然后自定义类行为的关键部分。
意识到 Django 字段类不是存储在模型属性中的内容,这一点很重要。 模型属性包含普通的 Python 对象。 创建模型类时,您在模型中定义的字段类实际上存储在 Meta
类中(具体如何完成的细节在这里并不重要)。 这是因为当您只是创建和修改属性时不需要字段类。 相反,它们提供了在属性值与存储在数据库中或发送到 序列化器 的内容之间进行转换的机制。
创建您自己的自定义字段时请记住这一点。 您编写的 Django Field
子类提供了以各种方式在 Python 实例和数据库/序列化器值之间进行转换的机制(例如,存储值和使用值进行查找之间存在差异)。 如果这听起来有点棘手,请不要担心 - 在下面的示例中它会变得更清楚。 请记住,当您需要自定义字段时,您通常会最终创建两个类:
- 第一类是您的用户将操作的 Python 对象。 他们会将它分配给模型属性,他们将从它中读取以用于显示目的,诸如此类。 这是我们示例中的
Hand
类。 - 第二个类是
Field
子类。 这个类知道如何在它的永久存储形式和 Python 形式之间来回转换你的第一个类。
编写一个 field 子类
在规划您的 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 的许多模型字段都接受它们不做任何事情的选项。 例如,您可以将 editable 和 auto_now 传递给 django.db.models.DateField,它只会忽略 editable参数(auto_now 被设置意味着 editable=False
)。 在这种情况下不会引发错误。
这种行为简化了字段类,因为它们不需要检查不必要的选项。 他们只是将所有选项传递给父类,然后不再使用它们。 您是希望字段对它们选择的选项更加严格,还是使用当前字段的更简单、更宽松的行为,这取决于您。
Field.__init__()
方法采用以下参数:
verbose_name
name
primary_key
max_length
unique
blank
null
db_index
rel
:用于相关字段(如 ForeignKey)。 仅供高级使用。default
editable
serialize
:如果是False
,模型传递给Django的serializers时,字段不会被序列化。 默认为True
。unique_for_date
unique_for_month
unique_for_year
choices
help_text
db_column
- db_tablespace:仅用于索引创建,如果后端支持tablespaces。 您通常可以忽略此选项。
- auto_created:
True
如果字段是自动创建的,至于模型继承使用的OneToOneField。 仅供高级使用。
上面列表中没有解释的所有选项都具有与普通 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)
更改自定义字段的基类
您无法更改自定义字段的基类,因为 Django 不会检测到更改并为其进行迁移。 例如,如果您从以下内容开始:
class CustomCharField(models.CharField):
...
然后决定使用 TextField
代替,您不能像这样更改子类:
class CustomCharField(models.TextField):
...
相反,您必须创建一个新的自定义字段类并更新您的模型以引用它:
class CustomCharField(models.CharField):
...
class CustomTextField(models.TextField):
...
如 删除字段 中所述,只要您有引用它的迁移,就必须保留原始 CustomCharField
类。
为自定义字段编写文档
与往常一样,您应该记录您的字段类型,以便用户知道它是什么。 除了为其提供对开发人员有用的文档字符串外,您还可以允许管理应用程序的用户通过 django.contrib.admindocs 应用程序查看字段类型的简短描述。 为此,只需在自定义字段的 description 类属性中提供描述性文本。 在上面的例子中,HandField
的 admindocs
应用程序显示的描述将是“一手牌(桥牌风格)”。
在 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'
db_type() 和 rel_db_type() 方法在框架为您的应用程序构建 CREATE TABLE
语句时被 Django 调用——也就是说,当您第一次创建表时. 在构造包含模型字段的 WHERE
子句时,也会调用这些方法——也就是说,当您使用 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() 方法由诸如 ForeignKey
和 OneToOneField
之类的字段调用,这些字段指向另一个字段以确定它们的数据库列数据类型。 例如,如果您有一个 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 转为查询值
由于使用数据库需要以两种方式进行转换,因此如果您覆盖 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)])
警告
如果您的自定义字段使用 MySQL 的 CHAR
、VARCHAR
或 TEXT
类型,则必须确保 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_now或者auto_now_add的情况下正确设置属性。
如果确实覆盖了此方法,则必须在最后返回该属性的值。 如果对值进行任何更改,您还应该更新模型的属性,以便保存对模型的引用的代码将始终看到正确的值。
为模型字段指定表单字段
要自定义 ModelForm 使用的表单字段,您可以覆盖 formfield()。
表单字段类可以通过 form_class
和 choices_form_class
参数指定; 如果字段指定了选项,则使用后者,否则使用前者。 如果未提供这些参数,则将使用 CharField 或 TypedChoiceField。
所有 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'
无论我们使用哪个数据库后端,这都意味着 :djadmin:`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 类型与数据库和序列化格式之间进行复杂转换时。 这里有一些技巧可以让事情进展得更顺利:
- 查看现有的 Django 字段(在
django/db/models/fields/__init__.py
中)以获得灵感。 尝试找到一个与您想要的字段相似的字段并对其进行一点扩展,而不是从头开始创建一个全新的字段。 - 将
__str__()
方法放在您作为字段包装的类上。 有很多地方字段代码的默认行为是在值上调用str()
。 (在本文档的示例中,value
将是Hand
实例,而不是HandField
)。 所以如果你的__str__()
方法自动转换为你的 Python 对象的字符串形式,你可以省去很多工作。
编写 FileField 子类
除了上述方法之外,处理文件的字段还有一些其他特殊要求必须考虑在内。 FileField
提供的大部分机制,例如控制数据库存储和检索,可以保持不变,留下子类来应对支持特定类型文件的挑战。
Django 提供了一个 File
类,用作文件内容和操作的代理。 可以将其子类化以自定义访问文件的方式以及可用的方法。 它位于 django.db.models.fields.files
,其默认行为在 文件文档 中有解释。
一旦创建了 File
的子类,必须告知新的 FileField
子类使用它。 为此,只需将新的 File
子类分配给 FileField
子类的特殊 attr_class
属性。
一些建议
除了上述细节之外,还有一些准则可以大大提高字段代码的效率和可读性。
- Django 自己的
ImageField
(在django/db/models/fields/files.py
中)的源代码是一个很好的示例,说明如何将FileField
子类化以支持特定类型的文件,因为它包含了所描述的所有技术以上。 - 尽可能缓存文件属性。 由于文件可能存储在远程存储系统中,因此检索它们可能会花费额外的时间,甚至金钱,这并不总是必要的。 一旦检索文件以获取有关其内容的一些数据,请尽可能多地缓存该数据,以减少在后续调用该信息时必须检索该文件的次数。