错误抛出
前端时间在生产环境上线了一个定时任务,目的是采集客户信息,以用来分析客户数据。2022-05-30 系统告警出现了错误。下面是错误信息:
2022-05-30 14:40:46,332 76 ERROR momo_prod odoo.addons.base.models.ir_cron: Call from cron Partner信息采集定时任务 for server action #417 failed in Job #17
Traceback (most recent call last):
File '/opt/momo/odoo/api.py', line 793, in get
return field_cache[record._ids[0]]
KeyError: 12
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File '/opt/momo/odoo/fields.py', line 972, in __get__
value = env.cache.get(record, self)
File '/opt/momo/odoo/api.py', line 796, in get
raise CacheMiss(record, field)
odoo.exceptions.CacheMiss: 'res.partner(75432,).group_id'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File '/opt/momo/odoo/addons/base/models/ir_cron.py', line 110, in _callback
self.env['ir.actions.server'].browse(server_action_id).run()
File '/opt/momo/odoo/addons/base/models/ir_actions.py', line 632, in run
res = runner(run_self, eval_context=eval_context)
File '/opt/momo/odoo/addons/base/models/ir_actions.py', line 501, in _run_action_code_multi
safe_eval(self.code.strip(), eval_context, mode='exec', nocopy=True) # nocopy allows to return 'action'
File '/opt/momo/odoo/tools/safe_eval.py', line 330, in safe_eval
return unsafe_eval(c, globals_dict, locals_dict)
File '', line 1, in <module>
File '/opt/momo/addons/momo_cron/models/cron_partner.py', line 199, in _action_cron_do_collection
collection_item.handle_record()
File '/opt/momo/addons/momo_cron/models/cron_partner.py', line 119, in handle_collection
parent_name = partner_res.group_id.name
File '/opt/momo/odoo/fields.py', line 2485, in __get__
return super().__get__(records, owner)
File '/opt/sps/odoo/fields.py', line 1004, in __get__
_('(Record: %s, User: %s)') % (record, env.uid),
odoo.exceptions.MissingError: Record does not exist or has been deleted.
(Record: res.partner(75432,), User: 1)
简单分析
定位到报错代码位置(line 119):
parent_name = partner_res.group_id.name
再看最后的错误信息:
odoo.exceptions.MissingError: Record does not exist or has been deleted.
(Record: res.partner(75432,), User: 1)
结合可知,错误并不是因为字段 group_id 可能没有值导致的问题,而是因为记录 partner_res 不存在或已删除导致的。
但这不对呀,我是判断了 partner_res 是否存在的:
partner_res = res_partner_obj.browse(self.partner_id)
if partner_res:
parent_name = partner_res.group_id.name
else:
parent_name = ""
如果 partner_res 不存在,就不应该走 partner_res.group_id.name ,更不应该报错。打个断点跟一下。
Debug
打上断点跟了一下不咋滴,这一跟简直跟出了一条惊天大案!
问题定位到了这句看起来非常普通的代码上:
partner_res = res_partner_obj.browse(self.partner_id)
断点过程发现 self.partner_id 是有值的,但数据库中确实不存在 id 为现 75432 的记录。怀疑问题出现在browse方法上,browse方法可能压根儿没走数据库。为了验证这个问题,我将问题代码简单抽出来复现一下。
问题复现
TIP: 已知在数据库中,表res_partner中不存在 id 为 999999 的记录,通过使用 browse 查找记录。
def match_record_by_browse(self):
"""
演示 browse 查询记录集
:return:
"""
browse_result = self.env["res.partner"].browse(999999)
print("browse_result", browse_result)
if browse_result:
print("browse_result yes")
else:
print("browse_result no")
终端输出
browse_result res.partner(999999,)
browse_result yes
可以看到数据库中,没有 id 为 999999 的记录,依然输出了 res.partner(999999,) 。
browse() API
查看下browse源码:
#
# Instance creation
#
# An instance represents an ordered collection of records in a given
# execution environment. The instance object refers to the environment, and
# the records themselves are represented by their cache dictionary. The 'id'
# of each record is found in its corresponding cache dictionary.
#
# This design has the following advantages:
# - cache access is direct and thus fast;
# - one can consider records without an 'id' (see new records);
# - the global cache is only an index to "resolve" a record 'id'.
#
@classmethod
def _browse(cls, env, ids, prefetch_ids):
""" Create a recordset instance.
:param env: an environment
:param ids: a tuple of record ids
:param prefetch_ids: a collection of record ids (for prefetching)
"""
records = object.__new__(cls)
records.env = env
records._ids = ids
records._prefetch_ids = prefetch_ids
return records
def browse(self, ids=None):
""" browse([ids]) -> records
Returns a recordset for the ids provided as parameter in the current
environment.
.. code-block:: python
self.browse([7, 18, 12])
res.partner(7, 18, 12)
:param ids: id(s)
:type ids: int or list(int) or None
:return: recordset
"""
if not ids:
ids = ()
elif ids.__class__ in IdType:
ids = (ids,)
else:
ids = tuple(ids)
return self._browse(self.env, ids, ids)
王德发!browse方法真的没有访问数据库,只是将提供的 id 包装成了当前环境的实例。目的是为了快速读取数据。
实例表示给定执行环境中的有序记录集合。实例对象引用环境,而记录本身由它们的缓存字典表示。每条记录的“id”都可以在其对应的缓存字典中找到。
这种设计具有以下优点:
缓存访问是直接的,因此速度很快。
可以考虑没有“id”的记录。
全局缓存只是“解析”记录“id”的索引。
这么来看,browse方法并不关心传入的 id 在相关表中是不是存在。它仅仅是为了将给定的 id 包装成ORM可用的实例。
问题解决
这是一个browse方法使用的问题。将browse方法替换为search方法,通过search方法访问数据库,查找是否真正存在即可解决此问题。
partner_res = res_partner_obj.search([("id", "=", self.partner_id)])