第一章 模型

我们在第二部分已经了解过模型的基础特性,这里我们将深入模型的底层架构,了解其更多的奥秘.

模型元类

Model的基类是BaseModel,而BaseModel的基类是MetaModel,MetaModel是一个元类,它的主要作用是注册每个模块中的模型。

class MetaModel(api.Meta):
    """ The metaclass of all model classes.
        Its main purpose is to register the models per module.
    """
    module_to_models = defaultdict(list)

    def __new__(meta,name,bases,attrs):
        # this prevents assignment of non-fields on recordsets
        attrs.setdefault('__slots__',())
        # this collects the fields defined on the class (via Field.__set_name__())
        attrs.setdefault('_field_definitions',[])

        if attrs.get('_register'True):
            # determine '_module'
            if '_module' not in attrs:
                module = attrs['__module__']
                assert module.startswith('odoo.addons.'),\
                    f"Invalid import of {module}.{name},it should start with 'odoo.addons'."
                attrs['_module'] = module.split('.')[2]

            # determine model '_name' and normalize '_inherits'
            inherit = attrs.get('_inherit',())
            if isinstance(inherit,str):
                inherit = attrs['_inherit'] = [inherit]
            if '_name' not in attrs:
                attrs['_name'] = inherit[0] if len(inherit) == 1 else name

        return super().__new__(meta,name,bases,attrs)

模型元类在创建对象的时候,会初始化_field_definitions变量用来记录该模型定义的字段列表. 然后检查模型是否已经注册,对于已经注册的模型,将会检查它是否包含模块信息 ,没有的话将会自动解析模块名称到_module变量中.

从代码中我们也可以知道,模型元类在获取模块信息时要求模块的注册路径中必须包含odoo.addons前缀,这个特性在13.0版本开始引入,在13.0之前模块的路径其实可以定义在其他地方,13.0以后这块就已经变得严格起来了.

另外,从这里我们也可以看到_inherit属性的本质上就是一个列表,只不过odoo做了兼容处理,虽然我们可以直接输入一个对象模型的名称,但实际上odoo还是将其转换为了列表进行处理.

接下来我们再看一下模型元类的初始化方法:

def __init__(self,name,bases,attrs):
    super().__init__(name,bases,attrs)

    if not attrs.get('_register'True):
        return

    # Remember which models to instantiate for this module.
    if self._module:
        self.module_to_models[self._module].append(self)

    if not self._abstract and self._name not in self._inherit:
        # this class defines a model: add magic fields
        def add(name,field):
            setattr(self,name,field)
            field.__set_name__(self,name)

        def add_default(name,field):
            if name not in attrs:
                setattr(self,name,field)
                field.__set_name__(self,name)

        add('id',fields.Id(automatic=True))
        add(self.CONCURRENCY_CHECK_FIELD,fields.Datetime(
            string='Last Modified on',automatic=True,
            compute='_compute_concurrency_field',compute_sudo=False))
        add_default('display_name',fields.Char(
            string='Display Name',automatic=True,compute='_compute_display_name'))

        if attrs.get('_log_access',self._auto):
            add_default('create_uid',fields.Many2one(
                'res.users',string='Created by',automatic=True,readonly=True))
            add_default('create_date',fields.Datetime(
                string='Created on',automatic=True,readonly=True))
            add_default('write_uid',fields.Many2one(
                'res.users',string='Last Updated by',automatic=True,readonly=True))
            add_default('write_date',fields.Datetime(
                string='Last Updated on',automatic=True,readonly=True))

与14.0不同,15.0中的魔法字段的定义放到了初始化方法_init_中,而14.0中则是放在BaseModel的_add_magic_fields方法中,15.0已经取消了这个方法.

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 (
            "<NewId origin=%r>" % self.origin if self.origin else
            "<NewId ref=%r>" % self.ref if self.ref else
            "<NewId 0x%x>" % id(self)
        )

NewId的代码很容易解读,从源代码中我们可以看到NewId对象只有两个属性:origin和ref。这两个属性记录了NewId的来源和关联内容。

如果对NewId进行布尔取值,那么它将永远返回Flase

NewId对象的相等比较,实际上是比的它的源记录和参考是否相等,只要其中一个相等,就判定为两个NewId是相等的.

NewId常见的使用场景是X2many的Onchange事件中,x2many的字段在更新时总是将原有的记录删除再创建一个新的,再尚未保存到数据库时,新的记录的只即NewId。

魔法字段

在模型中有一类字段会由系统自动生成,这就是魔法字段,当前版本的魔法字段主要由下面两种组成:

  • 日志访问字段: 即 create_uid,write_uid,create_date,write_date
  • 记录ID: id

正如前面代码所示,只有模型的_log_access显示为True或者_auto属性为真时,这些日志访问字段才会自动添加到模型中.

另外,我们在开发中也经常用到一个属性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()

inherit与inherits的区别

我们在第一部分第八章时介绍过inherit与inherits的使用方法,我们这里看一下它们的定义:

_inherit = ()
_inherits = frozendict()

inherit是一个元组,inherits 是一个不可变字典. 如果多个模型中定义了同名的字段, 那么关联的字段将会与inherits列表中的最后一个字段保持一致.

记录的有效或无效

我们知道,系统中有些字段可以被设置为归档(Archived)状态,处在归档状态的记录默认将不会被搜索到.

model中的_active_name属性就是来指定这个标识字段,默认情况下为None

如果某个模型种没有定义active字段同时也没有使用_active_name属性指定相应的字段,那么我们可以通过添加该字段从而是该模型拥有设置归档的功能.

active = fields.Boolean("Active",default=True)

CUID方法详解

create方法

关于create方法,我们其实已经很熟悉了, 但是真正了解create方法底层应用的同学可能并不多.今天我们就来详细地解读一下这些我们"最熟悉的陌生人".

首先,我们来温习一下, create方法的作用:

create方法用接收一个包含若干字段的参数来创建一条新记录.

下面是我们通常定义create方法的样子:

@api.model
def create(self, vals):
    ...
    return super().create(vals)

没什么特别的, 但是很多人不知道的是, vals实际上可以是一个数组, 也就是说create可以一次创建多条记录.

这在create方法的定义中显而易见:

@api.model_create_multi
@api.returns('self', lambda value: value.id)
def create(self, vals_list):
    """ create(vals_list) -> records
    Creates new records for the model.
    The new records are initialized using the values from the list of dicts
    ``vals_list``, and if necessary those from :meth:`~.default_get`.
    :param Union[list[dict], dict] vals_list:
        values for the model's fields, as a list of dictionaries::
            [{'field_name': field_value, ...}, ...]
        For backward compatibility, ``vals_list`` may be a dictionary.
        It is treated as a singleton list ``[vals]``, and a single record
        is returned.
        see :meth:`~.write` for details
    :return: the created records
    :raise AccessError: * if user has no create rights on the requested object
                        * if user tries to bypass access rules for create on the requested object
    :raise ValidationError: if user tries to enter invalid value for a field that is not in selection
    :raise UserError: if a loop would be created in a hierarchy of objects a result of the operation (such as setting an object as its own parent)
    """

那么api.model是如何处理多个记录的呢?

实际上, 这就要从我们的请求发起的情况讲起了:

Request从浏览器端发起以后, 由controller进行处理, controller首先根据按钮点击事件call_kw方法传递过来的模型和方法名称进行解析, 找到指定的模型M的指定方法F.

  • 如果F的装饰器是model: 那么则处理后返回记录集
  • 如果F的装饰器是model_create: 那么则处理后返回记录集的ids
  • 如果F的装饰器既不是model也不是model_create: 那么返回multi结果.

这里可能有同学要问了, 既然model和multi都是返回记录集, 那么他们的区别是什么?

答案就在两个装饰器的底层方法中:

def _call_kw_model(method, self, args, kwargs):
    context, args, kwargs = split_context(method, args, kwargs)
    recs = self.with_context(context or {})
    _logger.debug("call %s.%s(%s)", recs, method.__name__, Params(args, kwargs))
    result = method(recs, *args, **kwargs)
    return downgrade(method, result, recs, args, kwargs)

...

def _call_kw_multi(method, self, args, kwargs):
    ids, args = args[0], args[1:]
    context, args, kwargs = split_context(method, args, kwargs)
    recs = self.with_context(context or {}).browse(ids)
    _logger.debug("call %s.%s(%s)", recs, method.__name__, Params(args, kwargs))
    result = method(recs, *args, **kwargs)
    return downgrade(method, result, recs, args, kwargs)

看出来了吗, 他们的区别就是model方法装饰的record只是一个空的记录, 它的ids被刻意丢弃了.

瞬态模型的处理逻辑

我们知道瞬态模型(TransientModel)是会被系统定期进行清理,那么它的具体逻辑是如何的呢?

我们找到了基于时间和数量限制的逻辑.

@api.autovacuum
def _transient_vacuum(self):
    if self._transient_max_hours:
        # Age-based expiration
        self._transient_clean_rows_older_than(self._transient_max_hours * 60 * 60)

    if self._transient_max_count:
        # Count-based expiration
        self._transient_clean_old_rows(self._transient_max_count)

def _transient_clean_old_rows(self,max_count):
    # Check how many rows we have in the table
    query = 'SELECT count(*) FROM "{}"'.format(self._table)
    self._cr.execute(query)
    [count] = self._cr.fetchone()
    if count > max_count:
        self._transient_clean_rows_older_than(300)

def _transient_clean_rows_older_than(self,seconds):
    # Never delete rows used in last 5 minutes
    seconds = max(seconds,300)
    query = """
        SELECT id FROM "{}"
        WHERE COALESCE(write_date,create_date,(now() AT TIME ZONE 'UTC'))::timestamp
            < (now() AT TIME ZONE 'UTC') - interval %s
    """.format(self._table)
    self._cr.execute(query,["%s seconds" % seconds])
    ids = [x[0] for x in self._cr.fetchall()]
    self.sudo().browse(ids).unlink()

总结下来就是:

  • 如果临时表的记录存在时间长于配置中的限制时间,那么,这些记录将被直接删除. 但是系统会保留最后5分钟内创建/修改的记录,哪怕你配置中设置为1分钟.
  • 如果临时表的数据量大于配置中的数量限制,那么除5分钟以内被创建/修改的记录都将被删除,而不是只删除多余的记录.

也就是说5分钟的内的记录是不会被清理的,这是写死在代码里的逻辑.

获取字段

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方法的底层实现是_read方法, 现在我们就来详细分析_read方法内部的详细逻辑。

_read方法的作用主要有以下两种:

  • 用来从数据库中读取参数中传入的记录集的指定的字段,并将字段的值存到缓存中。
  • 同时,会将访问错误(AccessError)存在缓存中,非存储类型的字段将被忽略。
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)

_read方法的执行逻辑:

  1. _read方法内部会首先查看记录集是否为空,如果是空,那么将直接返回。
  2. 然后会检查对当前用户进行鉴权。
  3. 然后调用了flush方法将字段的更改更新到缓存中。这里是为了read方法跟在write方法之后覆盖掉write方法更新的值,因为read方法会从数据库获取值并刷新缓存。

接下来开始真正的读取操作,odoo会将当前存储类型的字段和继承的存储类型字段作为表的列名,组建sql查询语句,并执行查询操作。如果查询结果不为空,那么执行翻译操作,将查询结果根据当前上下文中的语言设置翻译成相应的值,并更新缓存中的值。

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
  1. 首先,read方法在内部对要读取的字段进行了分类。
  2. 对于存储类型字段和具有依赖字段的计算字段(预读且拥有访问权限), 调用_read方法进行读取,并将值更新到缓存中。
  3. 然后,read方法根据参数是否指定了经典读取模式决定是否调用name_get方法组织结果,最后将结果返回.

总结下来就是,read方法只对存储类型和具有依赖字段的字段生效,read方法会忽略掉非存储类型的字段. 对于非存储类型字段的读取方法,odoo采用了描述符协议的方式进行处理,具体请参考字段一章.

另外,我们可以看到对于存储类型和依赖的计算字段,都存储在了缓存中,那么odoo中的缓存是如何设计以及实现的呢?这里先挖一个坑,等我们继续旅行到缓存一节时就会揭开它神秘的面纱了。

方法的重载

最常见的重载基类的方法莫过于三大方法(create,write,unlink)了。

常规的重载方法

例如,我们重载create和write、unlink方法时,通常会像下面的这种方式写:

class Demo(models.Model):

    @api.model
    def create(self,vals):
        ......
        return super(Demo,self).create(vals)

我们可以使用super的这种方式来调用父类方法,也可以不调用,直接返回结果,也就是覆盖掉了父类的同名方法。

非常规的使用方法

考虑如下的场景:

我有一个类继承自产品模板类(product.template),有一个用来同步数据的方法A,在方法A的内部我使用了Write方法来更新产品数据,同时我还在类的内部重载了Write方法,但是我又希望调用A方法的时候不触发我重载的Write方法,怎么实现呢?

既然不希望调用本类下面的Write方法,那么就需要想办法绕过这个方法。同样是使用super方法,但是参数变了,变成父类即可。

文字可能说不清楚,下面来看代码:

from odoo.addons.orssica_base.models.models import ProductTemplate

class product_template(models.Model):

    _inherit = "product.template"

    def get_products_from_erp(self,from_date=None):
        ...
        super(ProductTemplate,product).write(prd_data)

    @api.multi
    def write(self,vals):
        """更新产品"""
        ...

如上所示,我们的get_products_from_erp方法中虽然调用了write方法,但是由于super的第一个参数是ProductTemplate而不是product_template,因此不会调用product_template的write方法,而是直接调用了ProductTemplate的write方法,因而实现了我们的需求。

基础模型的重载

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

from odoo import api, fields, models, _

class BaseModel(models.AbstractModel):

    _inherit = "base"

    ...

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

合理利用基础模型的重载将有利于我们减少代码的重复率,提高编程效率。举例来说,笔者在某个项目中经常需要用到使用向导来引导用户操作的方式,而使用向导的一个特性就是需要将当前的模型的记录传递到向导中,以方便向导初始化数据。

因为这个特性是非常常见的,因此我们可以考虑将这段代码做成通用的,放到BaseModel中,这样,其他模型便可以直接使用而不用在手动实现。

def _get_active_ids(self):
    active_model, active_ids = self._context.get("active_model"), self._context.get("active_ids")
    return self.env[active_model].browse(active_ids)

类似这种功能特性的代码,我们可以集中放到一个工具模块中以方便我们日后使用。

笔者这里提供了一个公开开源的基础模块,有兴趣的同学可以到我的github账号获取。

results matching ""

    No results matching ""