第一章 深入理解Odoo的API模型

api.model

api.model与普通的方法有何区别? 这个要从API的历史谈起, odoo还在使用python2的时候, 那个时候定义方法的参数很多, 写一个方法要带很多的参数, 像下面这样:

def funct(self,cr,uid,ids,context):
    ...

这样写代码真的很麻烦, 于是odoo在8.0时代推出了新的API接口, 使用装饰器的方法推出了下面几种装饰方法:

  • model
  • multi
  • one
  • constrains
  • depends
  • onchange
  • returns

api.model也就是在这个时候引进的, 这个时候api.model和api.multi,api.one还是单独分开的, 他们的主要区别也就是api.model返回的记录集self是一个不带有ids的空记录集, 有些类似python中的类方法的意味. 而api.multi返回的是一多行多个记录的记录集, api.one返回的是一个的单一记录.

从13.0开始, 官方又把multi和one一并砍掉, 类中定义的方法默认就是multi式的记录集.因此我们在写不带有装饰的方法时,一般都要遍历记录集 以防止传入的记录集是多个值的时候引起异常.

我们来看api.model的实现原理:

def model(method):
    """ Decorate a record-style method where ``self`` is a recordset, but its
        contents is not relevant, only the model is. Such a method::

            @api.model
            def method(self, args):
                ...

    """
    if method.__name__ == 'create':
        return model_create_single(method)
    method._api = 'model'
    return method

从这里我们可以看出来, 对于create方法, 如果使用了api.model进行装饰, 那么其内部将使用model_create_single方法来装饰该函数, 否则将使用标准的api.model逻辑. 我们先来看标准的api.model的逻辑是怎样的

api.model的内部实现

我们知道, 所有对象的方法调用都是从call_kw开始的. 那么我们来看看call_wk方法内部是如何处理的.

def call_kw(model, name, args, kwargs):
    """ Invoke the given method ``name`` on the recordset ``model``. """
    method = getattr(type(model), name)
    api = getattr(method, '_api', None)
    if api == 'model':
        result = _call_kw_model(method, model, args, kwargs)
    elif api == 'model_create':
        result = _call_kw_model_create(method, model, args, kwargs)
    else:
        result = _call_kw_multi(method, model, args, kwargs)
    model.flush()
    return result

这里的逻辑很清晰, 即

odoo在调用模型的方法时, 第一步会检查其是否包含_api属性,如果没有,那么按照多记录集的逻辑进行处理, 如果_api是model装饰的, 那么按照标准的api.model流程进行处理,而如果是model_create装饰的,那么按照创建方法的单例模式处理

由于api.model内部对于create方法做了特殊处理, 实际上api.model和api.model_create_single的效果是一样的.

由上可以知道,内部使用了_call_kw_model来处理api.model的逻辑:

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)

其中关键的一句在于:

recs = self.with_context(context or {})

与常规记录集不同的地方在于此, 这里将ids丢弃了, 传入method内部的只是一个模型,而不包含ids. 这样的结果就是 任何调用api.model方法装饰的方法, 其传入的self对象的ids都将会丢失.

api.model对于create方法的处理

如果使用api.model装饰了create方法, 那么它内部将调用model_create_single

def model_create_single(method):
    """ Decorate a method that takes a dictionary and creates a single record.
        The method may be called with either a single dict or a list of dicts::

            record = model.create(vals)
            records = model.create([vals, ...])
    """
    wrapper = decorate(method, _model_create_single)
    wrapper._api = 'model_create'
    return wrapper

def _model_create_single(create, self, arg):
    # 'create' expects a dict and returns a record
    if isinstance(arg, Mapping):
        return create(self, arg)
    if len(arg) > 1:
        _create_logger.debug("%s.create() called with %d dicts", self, len(arg))
    return self.browse().concat(*(create(self, vals) for vals in arg))

虽然model_create_single标识了应该使用单一模式, 但实际的代码中还是做多条记录做了兼容处理.同样的, 虽然model_create_multi也标识了用于创建多个记录, 但实际上其内部也对单一的记录做了兼容:

def model_create_multi(method):
    """ Decorate a method that takes a list of dictionaries and creates multiple
        records. The method may be called with either a single dict or a list of
        dicts::

            record = model.create(vals)
            records = model.create([vals, ...])
    """
    wrapper = decorate(method, _model_create_multi)
    wrapper._api = 'model_create'
    return wrapper

def _model_create_multi(create, self, arg):
    # 'create' expects a list of dicts and returns a recordset
    if isinstance(arg, Mapping):
        return create(self, [arg])
    return create(self, arg)

总结下来就是, 虽然odoo内部定义了model_create_single和model_create_multi两个装饰函数, 但实际上都对彼此做了兼容处理. 那么实际应用过程中, 我们在使用api.model标识create方法时 ,vals参数既可以是一个单例的字典 , 也可以是多个字典组成的列表 .

api.constrains

我们知道api.constrains用来给字段设置限制, 当字段发生变化时触发限制条件的验证. 那么api.constrains内部的触发机制是怎样的呢?

首先,我们需要知道, 在create和write方法中, 存在一个_validate_fields方法,该方法用来验证用户输入的数据是否合法. 而对_constrains条件的验证就是在这个方法内部中完成的:

def _validate_fields(self, field_names, excluded_names=()):
    """ Invoke the constraint methods for which at least one field name is
    in ``field_names`` and none is in ``excluded_names``.
    """
    field_names = set(field_names)
    excluded_names = set(excluded_names)
    for check in self._constraint_methods:
        if (not field_names.isdisjoint(check._constrains)
                and excluded_names.isdisjoint(check._constrains)):
            check(self)

即: 用户在调用create或write方法时会检查模型中的_constraint_methods属性中的方法,而如果页面中发生变化或创建时传入的字段正好在constrains的范围内, 那么就会触发此方法进行验证

而API中的contrains仅做了一件事,就是把字段或者函数挂在到模型实例中:

def constrains(*args):
    if args and callable(args[0]):
        args = args[0]
    return attrsetter('_constrains', args)

api.depends

最佳实践

create方法使用api.model装饰, vals值既可以传单个的字段 , 也可以传入多个字典组成的列表一次性创建多条记录

results matching ""

    No results matching ""