转载:http://book.odoomommy.com/chapter5/README20.html
第十六章 字段
我们在第一部分第三章时,已经认识过常见的几种字段类型以及它们的使用方法和应用场景,本章我们将深入了解字段的组成,并学会如何创建一个新的类型的字段.
字段的本质
首先,我们需要认识到,字段的本质其实也是一个类.我们在模型中定义的字段属性, 是通过Python中的描述符协议添加到属性实例中的,这一点与其他的流行框架(Django, Flask)并无不同.
其次,odoo中的字段类由一个元类派生而来,这个字段的元类(MetaField),定义了两个规则:
- 每个字段都必须有一个type属性,用来标识字段的类型
- 字段的属性被分成了关联性属性和描述性属性两类
- 每个特定类型的字段的_related_开头的属性将被添加到related_attrs属性中
- 每个特定类型的字段的_description_开头的属性将被添加到description_attrs属性中
关联性属性指的是在字段的定义中关联了其他对象的字段, 那么字段的属性将被存储在_related_开头的属性中. 描述性属性指的是常规的字段定义中的属性,通常是我们常见的属性,例如string,help,model_name等属性.
字段属性及其作用
我们在第一部分中简要的介绍过常见的几种属性及其作用,但是并不完整,下面我们将详细了解字段的属性和作用.
type
字段中最重要的属性, 在元类中强制规定了每个字段必须要定义的属性. 通常用来标识字段的类型,例如:
class Char(_String):
type = 'char'
...
此属性通常在定义字段类型的时候使用,我们在模型中实例化的字段属性已经由具体的字段类实现,因此不用关心此属性的值.
为了方便展示,我们这里创建了一个图书模块的shell环境:
book = self.env['book_store.book'].create({'name':"TEST"})
relational
标识改字段是否为关系字段.
>>>book._fields['name'].relational
False
translate
标识改字段是否已翻译
>>book._fields['name'].translate
False
column_type
数据库中的字段类型
>>book._fields['name'].column_type
('varchar', 'VARCHAR')
column_type是个与元组, 其第一个元素为字段的标识, 第二个元素为字段在数据库中的类型.
column_format
数据库查询语句中的占位符, 默认为%s.
>>book._fields['name'].column_format
'%s'
column_cast_from
可以被转换的类型
>>book._fields['name'].column_cast_from
('text',)
column_cast_from也是一个元组,其值是可以转换成此类型的其他数据库类型.
write_sequence
write方法调用时字段的写入顺序, 默认为0
>>book._fields['name'].write_sequence
0
args
用来初始化字段的参数
_module
字段的模块名称
>>> book._fields['name']._module
'book_store'
_modules
定义了该字段的魔窟列表
>>> book._fields['name']._modules
('book_store',)
_modules是个元组, 其内容是所有定义了该字段的模块列表
_setup_done
字段是否挂载完成, 默认为True
>>> book._fields['name']._setup_done
True
_sequence
字段的排序
>>> book._fields['name']._setup_done
True
_base_fields
15.0新增属性
重载字段的集合. 如果有多个模块同时定义了一个字段,那么这个字段的处理逻辑是将他们合并起来, 而_base_fields的作用就是记录这些重载的字段类型. 此字段对于toplevel的字段来说,但字段挂载完成以后,就会被置空以释放内存,因此对于direct和toplevel字段, 挂载后的值一直是空
>>> book._fields['serial_name']._base_fields
()
_extral_keys
15.0新增属性
设置字段时传入的未知字段.
_direct
15.0新增属性
是否可以被"直接"使用(共享的).
>>> book._fields['serial']._direct
True
_toplevel
15.0新增属性
>>> book._fields['name']._toplevel
False
>>> book._fields['serial_name']._toplevel
True
toplevel指的是只挂载一次, 一旦挂载完成将丢弃args和_base_fields内容, 因为他们不再需要这些数据了.
states
状态属性, 可以根据此属性设置改字段是否为只读或必填项. states属性的值是一个字典,key为readonly或required, 值是相应的state字段中的状态和布尔值组成的元组列表.
name = fields.Char("名称" ,readonly=True,states={'draft':[('readonly':False)]})
字段值的转换
字段类型中定义了一系列的"转换方法"来将字段的值转成不同的格式,以适应不同的应用场景. 字段类本身只是简单定义了这样一系列的方法, 具体到特定的字段类型时, 需要该类型的字段根据自身的需求重载这些方法以到达合适使用的目的.
convert_to_column
def convert_to_column(self, value, record, values=None, validate=True):
if value is None or value is False:
return None
return pycompat.to_text(value)
convert_to_column方法的作用是将value重新格式化为SQL可以使用的文本.
convert_to_record
def convert_to_record(self, value, record):
""" Convert ``value`` from the cache format to the record format.
If the value represents a recordset, it should share the prefetching of
``record``.
"""
return False if value is None else value
convert_to_record方法作用是将值从缓存的格式转换为记录集可以使用的格式.
convert_to_read
def convert_to_read(self, value, record, use_name_get=True):
""" Convert ``value`` from the record format to the format returned by
method :meth:`BaseModel.read`.
:param bool use_name_get: when True, the value's display name will be
computed using :meth:`BaseModel.name_get`, if relevant for the field
"""
return False if value is None else value
convert_to_read方法的作用是将值从记录集的格式转换为可以被ORM中的read方法返回的值. 该方法接受一个额外的参数user_name_get, 如果为True,那么字段的显示名称将使用name_get方法返回的值.
convert_to_write
def convert_to_write(self, value, record):
""" Convert ``value`` from any format to the format of method
:meth:`BaseModel.write`.
"""
cache_value = self.convert_to_cache(value, record, validate=False)
record_value = self.convert_to_record(cache_value, record)
return self.convert_to_read(record_value, record)
convert_to_write方法的作用是将任何格式的值,转换为可以被write方法使用的格式.
convert_to_onchange
def convert_to_onchange(self, value, record, names):
""" Convert ``value`` from the record format to the format returned by
method :meth:`BaseModel.onchange`.
:param names: a tree of field names (for relational fields only)
"""
return self.convert_to_read(value, record)
convert_to_onchange方法的作用是将值转换为可以被onchang方法返回的值格式.
convert_to_export
def convert_to_export(self, value, record):
""" Convert ``value`` from the record format to the export format. """
if not value:
return ''
return value
convert_to_export方法作用是将值转换为可以被导出的格式.
convert_to_display_name
def convert_to_display_name(self, value, record):
""" Convert ``value`` from the record format to a suitable display name. """
return ustr(value)
convert_to_display_name方法的作用是将值转为为合适的可以用来显示名称的格式.
描述符协议
我们在使用self.x的方式读取记录中某个字段的值的时候,实际上是使用了Python的描述符协议,Odoo把字段的获取逻辑也封装在了描述符协议中。接下来,我们详细看一下字段的读取过程。
odoo在读取某个字段时,会执行如下的逻辑:
if record is None:
return self # the field is accessed through the owner class
if not record._ids:
# null record -> return the null value for this field
value = self.convert_to_cache(False, record, validate=False)
return self.convert_to_record(value, record)
-
先判断当前记录是否为None,如果是None,则直接返回。
-
如果当前记录是空记录(没有ids),则返回一个空值。这就是我们之前测试的例子中,为什么有时候会出现计算字段的方法不会被触发的原因。
env = record.env
# only a single record may be accessed
record.ensure_one()
if self.compute and (record.id in env.all.tocompute.get(self, ())) \
and not env.is_protected(self, record):
# self must be computed on record
if self.recursive:
recs = record
else:
ids = expand_ids(record.id, env.all.tocompute[self])
recs = record.browse(itertools.islice(ids, PREFETCH_MAX))
try:
self.compute_value(recs)
except (AccessError, MissingError):
self.compute_value(record)
- 如果字段是存储的计算字段,则重新计算字段的逻辑(recompute)。
try:
value = env.cache.get(record, self)
except KeyError:
# real record
if record.id and self.store:
recs = record._in_cache_without(self)
try:
recs._fetch_field(self)
except AccessError:
record._fetch_field(self)
if not env.cache.contains(record, self) and not record.exists():
raise MissingError("\n".join([
_("Record does not exist or has been deleted."),
_("(Record: %s, User: %s)") % (record, env.uid),
]))
value = env.cache.get(record, self)
elif self.compute:
if env.is_protected(self, record):
value = self.convert_to_cache(False, record, validate=False)
env.cache.set(record, self, value)
else:
recs = record if self.recursive else record._in_cache_without(self)
try:
self.compute_value(recs)
except (AccessError, MissingError):
self.compute_value(record)
value = env.cache.get(record, self)
elif (not record.id) and record._origin:
value = self.convert_to_cache(record._origin[self.name], record)
env.cache.set(record, self, value)
elif (not record.id) and self.type == 'many2one' and self.delegate:
# special case: parent records are new as well
parent = record.env[self.comodel_name].new()
value = self.convert_to_cache(parent, record)
env.cache.set(record, self, value)
else:
value = self.convert_to_cache(False, record, validate=False)
env.cache.set(record, self, value)
defaults = record.default_get([self.name])
if self.name in defaults:
# The null value above is necessary to convert x2many field values.
# For instance, converting [(4, id)] accesses the field's current
# value, then adds the given id. Without an initial value, the
# conversion ends up here to determine the field's value, and this
# generates an infinite recursion.
value = self.convert_to_cache(defaults[self.name], record)
env.cache.set(record, self, value)
-
从缓存中尝试读取相应的字段值,如果命中异常,则执行下面的逻辑:
- 对于真实存储的字段,先从缓存中找出没有缓存该字段的记录集, 然后执行_fetch_fields方法,重新获取字段值, 从更新后的缓存中返回值
- 如果是计算值, 则重新触发计算逻辑后, 从缓存中返回值.
- 如果是关联字段, 则从关联字段中获取值后,更新到缓存中
- 如果是Many2one类型的委托字段, 则从委托对象中更新字段,并更新到缓存中
关系字段
关系字段读取过程中,如果实例属性是一个记录集,那么其本质上和使用mapped方法是一致的。
def __get__(self, records, owner):
# base case: do the regular access
if records is None or len(records._ids) <= 1:
return super().__get__(records, owner)
# multirecord case: use mapped
return self.mapped(records)