django “如何”系列3:如何编写模型域(model filed)
django自带很多的域类--CharField,DateField等等--,如果django的这些域都不能满足你精确的要求,那么你可以编写自己的模型域。
django自带的域没有和数据库列类型一一对应的,只有简单的VARCHAR,INTEGER等类型,为了使用更复杂的类型,例如多边形,你可以定义你的域子类;或者,你可能有一个很复杂的对象,这个对象可以通过某些方法序列化标准的数据库列类型,那么你可以定义自己的域子类。
示例对象
创建自定义的域需要注意一些特别的问题,为了使问题更简单的得以阐述,我们使用一个例子来说明吧。
我们使用一副扑克牌作为例子,类Hand代表一手牌,一手牌有52张,平均的发给四个玩家东南西北
class Hand(object): """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例子,我们可以把扑克牌数据转换成一个104的字符串(使用事先约定的顺序编号,如北东南西),所以Hand对象可以以文本或者字符的类型存储在数据库中。
一个域类是做什么的
我们这节里面说的域类,如果没有特别说明都是指模型域,而不是表单域。
django所有的域都是django.db.models.Field的子类。意识到django的域类并不似存储在你的模型的属性里面这一点非常重要,模型属性包含正常的python对象,当一个模型被创建的时候,你在一个模型里面定义的域类是被存储在Meta类的,这是因为当你仅仅是创建和修改属性的时候,域类并不是必需的,相反,域类只是提供了属性值和 数据库存储或者序列化 时候需要的转换机制而已。
当创建自己的域时,请记住上面这一点。记住,当你需要自定义一个域的时候,你只需要创建下面这两个类:
- 第一个类是用户可以操纵的python对象
- 第二个类是Field的子类
下面我们通过例子来讲述吧
编写一个field子类
当设计你的filed子类的时候,你首先要考虑的是有没有那双鞋已经存在的域类是比较接近你的需求的,如果有,请继承那个域类,否则,你只能继承Field这个比较底层的类了
初始化你的域类其实是一件:把你传进来的具体参数分离开来然后传给Filed(或者某个父类)的__init__()方法
在我们的例子中,我们把我们的域称为HandField,由于没有存在其他的域类接近我们的需求,我们直接继承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(HandField, self).__init__(*args, **kwargs)
在这个例子中,我们的HandField接受大部分的域的可选参数,下面我们会介绍,同时我们确保了它有一个精确的长度max_length,Field.__inti__()接受的参数如下:
- verbose_name
- name
- primary_key
- max_length
- unique
- blank
- null
- db_index
- rel: 用于相关域,想ForeignKey。仅供高级使用
- default
- editable
- serialize: 默认为真,如果是False,当该域传递给序列器的时候不会被序列化
- unique_for_date
- unique_for_month
- unique_for_year
- choices
- help_text
- db_column
- db_tablespace: 仅供索引创建,如果后端支持tablespaces的话,一般你可以忽略这个参数
- auto_created: 仅供高级使用
没特别说明,这些参数的意义和django默认的意义一致
SubfieldBase元类
处于两个原因,我们使用域的子类:利用通用的数据库列类型或处理复杂的python类型。显然,两者合二为一是有可能的,不够一般情况下你是用不到的。
如果你在处理自定义的python类型,比如我们的Hand类,我们需要确保,当django初始化一个我们模型的实例和给我们的自定义的域属性赋予数据库值的时候,我们可以把该值转换成适合的python对象。这个过程内部的实现有点复杂,但我们要写的代码是比较简单的:确保你使用了一个特定的元类。如下:
lass HandField(models.Field): description = "A hand of cards (bridge style)" __metaclass__ = models.SubfieldBase def __init__(self, *args, **kwargs): # ...
这保证了当属性被初始化的时候,to_python()方法会被调用
ModelForms和自定义域
如果你是使用SubfieldBase,to_python()将在每次域的实例被赋值的时候被调用,这意味着无论在哪里什么时候发生赋值,你必须保证这是一个正确的数据类型或者你有异常处理。
当你使用ModelForms的时候这点尤为重要,当保存一个ModelForm的时候,django将使用表单值去实例化模型实例。然而,如果清理后的表单数据不能作为有效的输入时,正常的表单验证过程将会被中断。
因此,你必须保证,用来表示你的自定义的域的表单域,无论是在执行输入验证还是数据清理,都需要把用户提供的表单输入转化成一个to_python()可以兼容的模型域值。这可能要求你编写一个自定义的表单域或者在你的域中实现formfield()方法以返回一个表达域类,该类的to_python()返回一个正确的数据类型。
文档你的自定义域
field.description
一如既往的,你应该为你的域类型编写文档,一遍让你的用户知道这是什么。使用field.description为你的域编写注释吧,这个注释可以被admindocs使用 。
有用方法
一旦你创建了你的Field子类和设置了__metaclass__,你可能考虑覆盖一些标准的方法,下面的方法按重要性排序
- Field.db_type(self,connection):考虑connection对象以及相关的配置,返回Field的数据库列数据类型。例如你已经创建了一个PostgreSQL自定义类型mytype,你可以通过继承Field然后实现db_type()方法来使用这个类型,例如:
from django.db import models class MytypeField(models.Field): def db_type(self, connection): return 'mytype'
一旦你有了MytypeField,你可以在任何一个模型中使用
class Person(models.Model): name = models.CharField(max_length=80) something_else = MytypeField()
如果你致力于创建一个数据库无关的应用,那么你应该注意不同数据库列类型中的不同之处,比如:日期类型在PostgreSQL叫timestamp,在MySQL中叫datetime,那么你可以这样写:
class MyDateField(models.Field): def db_type(self, connection):#检查connection.settings_dict['ENGINE']属性 if connection.settings_dict['ENGINE'] == 'django.db.backends.mysql': return 'datetime' else: return 'timestamp'
db_type()方法仅当第一次创建表的时候被django调用,而不会在其他的任何时候调用,因此它可以运行相当复杂的代码,比如检查connection.settings_dict['ENGINE']。
一些数据库列属性接受参数,比如CHAR(25),25代表最大列长,在这里你可以这样写,虽然有点别扭和牵强。
# 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()
一个更好的方法是动态传递参数,你可以通过实现__init__()来达到动态传递参数的效果
#有点复杂的例子 class BetterCharField(models.Field): def __init__(self, max_length, *args, **kwargs): self.max_length = max_length super(BetterCharField, self).__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创建代码去跳过这个域,你应该通过其他的方法在对的表里面创建这个列。
- Field.to_python(self,value):把数据库或者序列器返回的值转换成一个python对象。默认的实现只是简单的返回一个值,因为通常情况下数据库后端会以正确的python格式返回值。也因此,如果你的返回值是比python默认数据类型更复杂的话,你需要覆盖这个方法,按照一般的规则,这个方法应该优雅的处理一下的参数:
-
- 正确类型的一个实例
- 一个字符串
- 数据库返回的任何你正在使用的列类型
import re class HandField(models.Field): def to_python(self, value): if isinstance(value, Hand): return value # The string case. p1 = re.compile('.{26}') p2 = re.compile('..') args = [p2.findall(x) for x in p1.findall(value)] return Hand(*args)
- Field.get_prep_value(self,value):和to_python()相反(当与数据库后端配合的时候).