【转载】odoo技术开发白皮书 第二部分 第二章 模型

转载:http://book.odoomommy.com/chapter2/README9.html

第二章 模型

odoo中的三种model类型

  • TransientModel
  • AbstractModel
  • Model

TransientModel

TransientModel是指一种临时对象,它的数据将被系统定期清理,因此这就决定了它的使用场景不可以作为数据的持久化使用,只能作为临时对象使用。在odoo中,TransientModel最常被使用的一种场景是作为向导。

向导是odoo中常见的一种操作引导方式,该方式通常由一个弹出式的窗体和若干字段、按钮组成。用户通过向导可以选择指定特定的字段值,然后进行下一步的操作。向导背后的技术即用到了TransientModel,作为一种临时性的数据存储方案,向导的数据不会长期留存在数据库中,会由系统定期进行清理。

Model

与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

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)

从元类的定义中我们可以知道:

  • _register属性标识了该模型是否已经在系统中注册。
  • _module: 记录了当前模型所在的模块名称

NewId

在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

BaseModel是MetaModel的子类,是AbstractModel、TransientModel和Model的父类。

class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
    ...

系统会自动初始化一个每个数据库中的已经注册的模型的实例,这些实例代表了每个数据库中可以使用的模型。每个实例都是一个有序的记录集,可以通过browse或者search等方法返回包含符合条件的记录的记录集。

如果不希望系统在初始化的时候实例化某个模型,那么可以将它的_register属性设置为False.

基础属性

BaseModel包含了一些技术性的参数:

  • _auto: 是否自动创建数据库中的数据表,默认False(Mdoel中为True)
  • _regitster: 是否注册,不注册将在ORM中不可用
  • _abstract: 是否是抽象模型
  • _transient:是否是临时模型
  • _table: 指定数据库中的表名
  • _sequence: Id字段使用的SQL中的序号
  • _sql_constraints:数据库中的限制条件
  • _date_name: 日历视图中使用的字段,默认date
  • _fold_name: 看板视图中使用的字段,默认fold

_auto指定了该模型是否自动创建数据库表,如果设置False,那么用户需要在init方法中自行创建数据库表。

BaseModel中定义了4个魔法字段,即出现在每个数据库表中的字段:

  • create_uid:该条记录的创建人
  • create_date: 该条记录的创建时间
  • update_uid: 该条记录的更新人
  • update_date: 该条记录的更新时间
  • display_name: 显示名称

odoo中的每个注册完成的模型都会将自己的信息添加到系统记录中,具体来说:

  • ir.model: 会将自己的反射数据插入到ir_model表中
  • ir.model.fields:会将字段数据插入到ir_model_fields表中
  • ir.model.fields.selection: 将字段的可选项插入到ir_model_fields_selection表中(此特性在13.0引入)
  • ir.model.constraint: 将模型的约束插入到ir_modeLconstraint表中。

模型的继承

正如第一部分第四章介绍的,模型的继承是通过属性_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方法

_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语句查询出来的结果跟参数中的记录是否一致,如果不一致,那么将抛出异常,具体地将就是:

  • 如果记录集与查询结果集相交,那么说明查询出了不应该查询出来的结果,可能是并行计算出了问题,因此会抛出一个Access异常的错误。
  • 如果记录集包含查询结果集,那么说明其中有某些记录因为记录规则的问题被过滤掉了,因此会抛出规则读取异常。

现在, 我们回过头来看一下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: 需要被更新的记录集

如果没有传入任何参数,将更新所有挂起的计算。如果传入了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: 需要重新计算值的字段名列表
  • records: 需要重新计算值的记录集

如果不传fnames,则所有需要重新计算的字段都将重新计算。

BaseModel的重载

在第一部分,我们知道了模型可以被继承,方法可以被重载,这都是对常规的类来说的,如果我们有种需求要对基类的某些方法进行重载,该怎么做呢?可能有同学想到了,直接在源码的修改,但这是我们极为不推荐的一种方式。在比较早的版本,实现这个需求比较绕,要用到_register_hook方法,所幸的是从10.0开始,官方给我们提供了一种修改BaseModel的途径。

from odoo import api, fields, models, _

class BaseModel(models.AbstractModel):

    _inherit = "base"

    ...

我们只需要新建一个抽象类,然后继承自"base"模块,就可以对BaseModel中的方法进行重载了。

Model的属性

  • _rec_name: _rec_name用于指定显示在Many2one类型的搜索中的显示字段,可以简单地理解为该模型的名称。默认情况下,_rec_name取的是name字段。

魔法字段(Magic Field)

Odoo的每个模型可以自动记录创建和更新的信息,使用_log_access属性来标志是否启用,如果启用,模型将自动添加相关的字段,这些字段是:

  • id :记录的ID
  • create_uid : 记录创建者的ID
  • create_date:记录创建时间
  • write_uid:记录更新者ID
  • write_date:记录更新者的时间

被称为魔法字段,上面五个字段是存储在数据库中的。

另外, 我们在开发中也经常用到一个属性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字段的作用是根据自定义的计算方法来计算并发性字段的的值。

根据源码分析可以得出结论:

  • 如果模型启用了日志记录功能,那么最后的更新时间的值根据优先级取自 write_date> create_date > fields.Datetime.now()
  • 如果模型没有启用日志记录功能,那么最后的更新时间值就是fields.Datetime.now()

 

你可能感兴趣的:(【转载】odoo技术开发白皮书,erp)