转载:http://book.odoomommy.com/chapter2/README9.html
TransientModel是指一种临时对象,它的数据将被系统定期清理,因此这就决定了它的使用场景不可以作为数据的持久化使用,只能作为临时对象使用。在odoo中,TransientModel最常被使用的一种场景是作为向导。
向导是odoo中常见的一种操作引导方式,该方式通常由一个弹出式的窗体和若干字段、按钮组成。用户通过向导可以选择指定特定的字段值,然后进行下一步的操作。向导背后的技术即用到了TransientModel,作为一种临时性的数据存储方案,向导的数据不会长期留存在数据库中,会由系统定期进行清理。
与TransientModel相对的就是数据的持久化存储方案,即Model对象,odoo中的绝大多数对象都是由Model继承而来的。
Model是继承基础模型(BaseModel)而来, 基础模型是odoo所有类的基类。实例化类对象的方式即继承自Model、TransientModel或AbstractModel中的一种。每个类的实例都是一个有序的记录集合(RecordSet)。如果希望创建一个不被实例化的类,可以把_register属性设置为False。
默认情况下,Model和TransientModel的子类在初始化的过程中都会自动创建数据库表,如果不希望自动创建数据库表,可以将类的_auto属性设置为False。但创建没有数据库表的模型的更推荐的方式是使用抽象类(AbstractModel)。
常用的模型属性
_name: 每个模型都会由一个名称,使用属性_name设置。通常使用.分隔,例如sale.order,通常该模型会被对应为数据库中的表sale_order。
_description: 模型的描述信息
_inherit: 继承自的模型列表(详情参考第一部分第四章)
_table: 指定数据库中生成的表名
__sql_constraints: 指定sql约束[(name,约束,信息)]
_rec_name: 标签名称,即默认使用指代本对象的字段名称,默认为name
_order: 排序依据,默认为id字段
_parent_name: 父级字段 Many2one字段用作父字段
_parent_store: 是否存储父级路径。(参考第三部分 嵌套集模型一章)
MetaModel是BaseMOdel的基类,它是一个元类。它的主要作用是注册每个模块中的模型。
14.0版本以前 模型注册不会校验文件路径,14.0开始,元类中多了一个模块路径检查,即强制模块必须包含在odoo.addons路径中,否则不会注册成功。
def __init__(self, name, bases, attrs):
if not self._register:
self._register = True
super(MetaModel, self).__init__(name, bases, attrs)
return
if not hasattr(self, '_module'):
self._module = self._get_addon_name(self.__module__)
# Remember which models to instanciate for this module.
if not self._custom:
self.module_to_models[self._module].append(self)
# check for new-api conversion error: leave comma after field definition
for key, val in attrs.items():
if type(val) is tuple and len(val) == 1 and isinstance(val[0], Field):
_logger.error("Trailing comma after field definition: %s.%s", self, key)
if isinstance(val, Field):
val.args = dict(val.args, _module=self._module)
从元类的定义中我们可以知道:
在odoo中,如果你创建了一个记录但尚未保存,那么系统会自动分配给当前新记录一个临时的ID,这个ID的类型即NewId。我们先来看一下 NewId的定义:
class NewId(object):
""" Pseudo-ids for new records, encapsulating an optional origin id (actual
record id) and an optional reference (any value).
"""
__slots__ = ['origin', 'ref']
def __init__(self, origin=None, ref=None):
self.origin = origin
self.ref = ref
def __bool__(self):
return False
def __eq__(self, other):
return isinstance(other, NewId) and (
(self.origin and other.origin and self.origin == other.origin)
or (self.ref and other.ref and self.ref == other.ref)
)
def __hash__(self):
return hash(self.origin or self.ref or id(self))
def __repr__(self):
return (
"" % self.origin if self.origin else
"" % self.ref if self.ref else
"" % id(self)
)
NewId的代码很容易解读,从源代码中我们可以看到NewId对象只有两个属性:origin和ref。这两个属性记录了NewId的来源和关联内容。如果对NewId进行布尔取值,那么它将永远返回Flase。
NewId常见的使用场景是X2many的Onchange事件中,x2many的字段在更新时总是将原有的记录删除再创建一个新的,再尚未保存到数据库时,新的记录的只即NewId。
BaseModel是MetaModel的子类,是AbstractModel、TransientModel和Model的父类。
class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
...
系统会自动初始化一个每个数据库中的已经注册的模型的实例,这些实例代表了每个数据库中可以使用的模型。每个实例都是一个有序的记录集,可以通过browse或者search等方法返回包含符合条件的记录的记录集。
如果不希望系统在初始化的时候实例化某个模型,那么可以将它的_register属性设置为False.
BaseModel包含了一些技术性的参数:
_auto指定了该模型是否自动创建数据库表,如果设置False,那么用户需要在init方法中自行创建数据库表。
BaseModel中定义了4个魔法字段,即出现在每个数据库表中的字段:
odoo中的每个注册完成的模型都会将自己的信息添加到系统记录中,具体来说:
正如第一部分第四章介绍的,模型的继承是通过属性_inherit来实现的。
parents = cls._inherit
parents = [parents] if isinstance(parents, str) else (parents or [])
# determine the model's name
name = cls._name or (len(parents) == 1 and parents[0]) or cls.__name__
从源码中可以看出_inherit希望传入的是一个模型的列表,假如只有一个父类模型,那么也可以传入文本,且指定了_inherit时可以不声明_name,系统会自己使用模型的父类的_name。
BaseModel内部封装了从数据库中获取字段的方法, 该方法是_fetch_field.
def _fetch_field(self, field):
""" Read from the database in order to fetch ``field`` (:class:`Field`
instance) for ``self`` in cache.
"""
self.check_field_access_rights('read', [field.name])
# determine which fields can be prefetched
if self._context.get('prefetch_fields', True) and field.prefetch:
fnames = [
name
for name, f in self._fields.items()
# select fields that can be prefetched
if f.prefetch
# discard fields with groups that the user may not access
if not (f.groups and not self.user_has_groups(f.groups))
# discard fields that must be recomputed
if not (f.compute and self.env.records_to_compute(f))
]
if field.name not in fnames:
fnames.append(field.name)
self = self - self.env.records_to_compute(field)
else:
fnames = [field.name]
self._read(fnames)
_fetch_field方法从数据库中获取字段数据时,首先会调用check_field_access_right方法用来检查当前用户是否有权读取该字段. 然后_fetch_field会结合当前上下文中的prefetch_fields字段来判断该字段是否需要预读, 然后调用_read方法读取字段内容.
_read方法是ORM的read方法的内部实现方法,用来从数据库中读取参数中传入的记录集的指定的字段,并将字段的值存到缓存中。同时,_read方法会将访问错误(AccessError)存在缓存中,非存储类型的字段将被忽略。
_read方法内部会首先查看记录集是否为空,如果是空,那么将直接返回。然后会检查当前用户是否有权进行读取。
然后调用了flush方法将字段的更改更新到缓存中。这里是为了read方法跟在write方法之后覆盖掉write方法更新的值,因为read方法会从数据库获取值并刷新缓存。
if not self:
return
self.check_access_rights('read')
# if a read() follows a write(), we must flush updates, as read() will
# fetch from database and overwrites the cache (`test_update_with_id`)
self.flush(fields, self)
接下来开始真正的读取操作,odoo会将当前存储类型的字段和继承的存储类型字段作为表的列名,组建sql查询语句,并执行查询操作。如果查询结果不为空,那么执行翻译操作,将查询结果根据当前上下文中的lang设置翻译成相应的值,并更新缓存中的值。
missing = self - fetched
if missing:
extras = fetched - self
if extras:
raise AccessError(
_("Database fetch misses ids ({}) and has extra ids ({}), may be caused by a type incoherence in a previous request").format(
missing._ids, extras._ids,
))
# mark non-existing records in missing
forbidden = missing.exists()
if forbidden:
raise self.env['ir.rule']._make_access_error('read', forbidden)
最后,_read方法会校验sql语句查询出来的结果跟参数中的记录是否一致,如果不一致,那么将抛出异常,具体地将就是:
现在, 我们回过头来看一下ORM一章中提到的read方法的具体逻辑:
fields = self.check_field_access_rights('read', fields)
# fetch stored fields from the database to the cache
stored_fields = set()
for name in fields:
field = self._fields.get(name)
if not field:
raise ValueError("Invalid field %r on model %r" % (name, self._name))
if field.store:
stored_fields.add(name)
elif field.compute:
# optimization: prefetch direct field dependencies
for dotname in field.depends:
f = self._fields[dotname.split('.')[0]]
if f.prefetch and (not f.groups or self.user_has_groups(f.groups)):
stored_fields.add(f.name)
self._read(stored_fields)
# retrieve results from records; this takes values from the cache and
# computes remaining fields
data = [(record, {'id': record._ids[0]}) for record in self]
use_name_get = (load == '_classic_read')
for name in fields:
convert = self._fields[name].convert_to_read
for record, vals in data:
# missing records have their vals empty
if not vals:
continue
try:
vals[name] = convert(record[name], record, use_name_get)
except MissingError:
vals.clear()
result = [vals for record, vals in data if vals]
return result
首先, read方法在内部对要读取的字段进行了分类, 对于存储类型字段和具有依赖字段的计算字段(预读且拥有访问权限), 调用_read方法进行读取, 并将值更新到缓存中. 然后, read方法根据参数是否指定了经典读取模式决定是否调用name_get方法组织结果, 最后将结果返回.
总结下来就是, read方法只对存储类型和具有依赖字段的字段生效, read方法会忽略掉非存储类型的字段. 对于非存储类型字段的读取方法, odoo采用了描述符协议的方式进行处理,具体请参考字段一章.
BaseModel中定义了一些常用的方法:
user_has_groups
判断用户是否在某个用户组中。
@api.model
def user_has_groups(self, groups):
....
groups是用户组的xmlid列表,用,分隔,例如:user.user_has_groups('`base.group_user,base.group_system')
with_context
with_context方法用来给当前记录集对象扩展新的上下文对象。
# 当前context对象内容{'key1':True}
r2 = records.with_context({},key2=True)
# r2._context 内容:{'key2':True}
r2 = records.with_context(key2=True)
# r2._context内容:{'key1':True,'key2':True}
如果当前上下文中对象_context中包含allowed_company_ids 那么,with_context方法在返回的结果中也会包含allowed_company_ids。
flush方法
BaseModel中还定义了一个flush方法,其作用是将所有挂起的计算更新到数据库中。
@api.model
def flush(self, fnames=None, records=None):
""" Process all the pending computations (on all models), and flush all
the pending updates to the database.
:param fnames (list): list of field names to flush. If given,
limit the processing to the given fields of the current model.
:param records (Model): if given (together with ``fnames``), limit the
....
flush方法接收两个参数:
如果没有传入任何参数,将更新所有挂起的计算。如果传入了fnames,将更新当前模型中需要被更新的字段列表。如果传入了records,将更新指定的记录集的值。
new方法
产生一个使用传入的值并附加在当前环境的新记录值。此值只存在内存中,尚未保存到数据库中。
@api.model
def new(self, values={}, origin=None, ref=None):
""" new([values], [origin], [ref]) -> record
Return a new record instance attached to the current environment and
initialized with the provided ``value``. The record is *not* created
in database, it only exists in memory.
One can pass an ``origin`` record, which is the actual record behind the
result. It is retrieved as ``record._origin``. Two new records with the
same origin record are considered equal.
One can also pass a ``ref`` value to identify the record among other new
records. The reference is encapsulated in the ``id`` of the record.
"""
if origin is not None:
origin = origin.id
record = self.browse([NewId(origin, ref)])
record._update_cache(values, validate=False)
return record
从函数定义中,我们可以看到new方法返回一个记录值,该记录值的ID是我们前面提到过的NewId对象。
recompute
重新计算所有或指定的字段值。
@api.model
def recompute(self, fnames=None, records=None):
""" Recompute all function fields (or the given ``fnames`` if present).
The fields and records to recompute have been determined by method
:meth:`modified`.
"""
...
recompute接收两个参数:
如果不传fnames,则所有需要重新计算的字段都将重新计算。
BaseModel的重载
在第一部分,我们知道了模型可以被继承,方法可以被重载,这都是对常规的类来说的,如果我们有种需求要对基类的某些方法进行重载,该怎么做呢?可能有同学想到了,直接在源码的修改,但这是我们极为不推荐的一种方式。在比较早的版本,实现这个需求比较绕,要用到_register_hook方法,所幸的是从10.0开始,官方给我们提供了一种修改BaseModel的途径。
from odoo import api, fields, models, _
class BaseModel(models.AbstractModel):
_inherit = "base"
...
我们只需要新建一个抽象类,然后继承自"base"模块,就可以对BaseModel中的方法进行重载了。
Odoo的每个模型可以自动记录创建和更新的信息,使用_log_access属性来标志是否启用,如果启用,模型将自动添加相关的字段,这些字段是:
被称为魔法字段,上面五个字段是存储在数据库中的。
另外, 我们在开发中也经常用到一个属性ids, 它虽然不是由字段定义的, 但它跟id字段的关系却很密切。
@property
def ids(self):
""" Return the list of actual record ids corresponding to ``self``. """
return list(origin_ids(self._ids))
另外,还有一个被称为CONCURRENCY_CHECK_FIELD的计算字段,不存储在数据库中,CONCURRENCY_CHECK_FIELD字段的作用是根据自定义的计算方法来计算并发性字段的的值。
根据源码分析可以得出结论: