编写自定义模型字段(model fields)
介绍
字段参考 文档介绍了如何使用 Django 的标准字段类—— CharField
, DateField
,等等。大多数情况下,这些类就是你需要的。虽然有时候,Django 的版本不能精确地匹配你的需求,或者你想使用的字段与 Django 内置的完全不同。
Django 内置的字段类型并未覆盖所有可能的数据库字段类型——只有常见的类型,例如 VARCHAR
和 INTEGER
。对于更多模糊的列类型,例如地理多边形(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) ...
这仅是个普通的 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 对象的方式展示——字符串,布尔值, datetime
,或其它更复杂的东西,比如 Hand
——将它与某种格式之间来回转换,这在处理数据库(或序列化)时很有用(但是,像我们稍后看到的,一旦您控制了数据库端,就会很自然地出现这种情况)。
模型中的字段必须能以某种方式转换为已存在的数据库列类型。不能的数据库提供不同的可用列类型集,但规则仍相同:你只需要处理这些类型。你想存在数据库中的任何数据都必须能适配这些类型中的某一个。
一般来说,你可以编写一个 Django 字段来适配特定是数据库列类型,或者已存在一个直接承载你数据的类型,例如,字符串。
对于我们的 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 个字符。
注解
许多 Django 模板接收一堆不处理的选项。你可以同时给 django.db.models.DateField
传入 editable
和 auto_now
,它会直接忽略 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 的 序列化器 时不会被序列化。默认为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()
非常简单;它返回包含 4 个项目的原则:字段属性名,字段类的完整导入路径,可选参数(列表),和关键字参数(字典)。注意,这与 为自定义类 的 deconstruct
方法不同,它返回包含 3个项目的元组。
作为自定义字段的作者,你不需要担心前两个值;基类 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
中设置了新的默认值需要额外注意;说明你希望总是包含它们,而不是在它们采用旧有值时消失。
也就是说,尽量避免以位置参数返回值;可能的话,尽量以关键字参数返回,未来能更好的扩展。当然,如果你在构造器的参数列表中,修改参数名比参数位置更频繁的话,你可能更倾向于位置参数,但是,牢记于心,未来别人会从序列化版本中重新构建你的字段,可能是一小会后(可能数年后),这取决于你的迁移持续的时间。
你能查看析构的结果,通过观察包含字段的迁移。你也能在单元测试中测试析构,只要简单的测试析构和重构字段:
- 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
类只要你还有迁移指向它。
为自定义字段编写文档
像之前一样,你需要为自定义字段类型编写文档,这样用户就会知道这他喵到底是啥。除了为其提供 docstring (对开发者很有用)外,你也需要让后台用户通过 django.contrib.admindocs 看到一个关于字段类型的简单介绍。只需简单地在自定义字段的 description
属性提供描述性文本。在上述例子中,由 admindocs
应用为 HandField
字段提供的描述是 '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()
如果你意在构建一个兼容各种数据库的应用,你需要了解不同数据库列之间的差异。举个例子,PostgreSQL 中的 date/time 列类型叫做 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()
方法由 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()
更好的方式是在运行时指定参数值——即类实例化的时候。只需像这样实现 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()
返回字符串类型的时候出现。
将查询值转为数据库值
某些数据类型(比如 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 的 DateTimeField
在 auto_now
或 auto_now_add
中利用此方法正确设置属性。
如果你重写了此方法,你必须在最后返回该属性的值。如果修改了值,那么你也需要更新模型属性,这样持有该引用的模型总会看到正确的值。
为模型字段指定表单字段
为了自定义 ModelForm
使用的表单属性,你必须重写 formfield()
。
表单字段类能通过 form_class
和 choices_form_class
参数指定;如果字段指定了选项,则使用后者,反之前者。若未提供这些参数,将会使用 CharField
或 TypedChoiceField
。
完整的 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 类,数据库,序列化格式之间进行复杂转换的时候。下面有几个让事情更顺利的建议:
- 借鉴已有的 Django 字段(位于
django/db/models/fields/init.py
)。试着找到一个与你目标类似的字段,而不是从零开始创建。 - 为字段类添加一个
str()
方法。在很多地方,字段代码的默认行为是对值调用str()
。(本页文档中,value
会是一个Hand
实例,而不是HandField
)。所以__str()
方法会自动将 Python 对象转为字符串格式,帮你剩下不少时间。
编写一个 FileField 子类
除了上述方法外,处理文件的字段还有一些必须考虑到的特殊要求。 FileField
提供的大部分机制(像是操作数据库存储和检索)能保持不变,让子类面对支持特殊文件的挑战。
Django 提供一个 File
类,作为文件内容和文件操作的代理。可以继承该类自定义访问文件的方式,哪些方法是可用的。它位于 django.db.models.fields.files
,它的默认行为在 file 文档 中介绍。
一旦创建了 File
子类,必须说明要使用新子类 FileField
。为此,只需为 FileField
的子类的 attr_class
指定新的 File
子类。
一些建议
除了上述细节,下面还有一些准则,有助于极大地提高字段代码的效率和可读性。
- Django 的
ImageField
的源码(位于django/db/models/fields/files.py
)就是个展示如何继承FileField
支持特定文件的不错例子,因为它包含了上述所有技巧。 - 尽可能的缓存文件属性。因为文件可能保存在远端存储系统中,检出它们会消耗额外的时间,甚至是钱,且不总是必要的。一旦检出某个文件,获取其内容,尽可能缓存所有数据,以减少后续调用再次检索文件的次数。