第一章 ORM
odoo开发的大多数场景都是基于它的ORM框架进行的, 一少部分要求性能的场景才会涉及到原生SQL的使用。 本章我们将带大家认识几种常见的基本的ORM方法, 可以满足我们大部分场景的开发需求。
下面是一个方法的列表, 可以点击迅速浏览相应的介绍。最基础的四大方法(browse,create,write,unlink)已经在第一部分介绍过了,本章不再赘述.
- read
- read_group
- search
- search_read
- name_get
- name_search
- fields_get
- load_views
- fields_view_get
- fields_get
- mapped
- sorted
- concat
- default_get
- copy
- get_formview_action
read方法
read方法的作用是根据请求的字段返回相应的记录集中的值。read方法是一种低阶的RPC方法, 我们开发中通常用的到是browse方法。
def read(self, fields=None, load='_classic_read'):
...
该方法接收两个非必填的参数:
- fields: 要读取的字段列表,默认为None,代表读取全部字段。
- load: 数据加载模式,默认为经典模式(_classic_read),与之相对的是新模式。
我们来举一个典型的例子, 在开发过程中,我们常会遇到点击一个按钮,打开一个包含若干记录的视图, 即form表单中的状态按钮(state button)的作用。这种按钮的后端代码通常是一个方法,返回了一个既定的窗口动作, 而获取这个窗口动作就用到了read方法.
def button_open_wizard(self):
"""派单"""
action = self.env.ref('juhui_repairs.action_repair_wizard').read()[0]
return action
read方法在没有传入fields的情况下,将获取当前用户拥有访问权限的所有字段(对于超级管理员来说,就是所有字段)。如果用户没有对传入的fields的访问权限,那么将引发AccessDenied错误。
实际上,read方法内部是通过check_fields_access_rights方法对用户进行鉴权的,也是通过此方法将用户可以访问的字段返回的,这也就是为什么read方法本身并没有对fields进行None值判断却允许fields为None时返回全部可访问的字段列表的原因。 不仅如此,read方法还将数据库中存储的字段进行了缓存,以加快访问速度。
下面我们在我们的豆瓣图书应用中新增一个按钮来演示read方法的作用及其结果。
def button_read(self):
"""ORM Read"""
res = self.read()
print('---Read---')
print(res)
然后我们可以在页面中点击Read按钮来查看它的输出:
[
{
'id': 1,
'name': '冰与火之歌',
'title': '(1-5卷:权力的游戏、列王的纷争、冰雨的风暴、群鸦的盛宴、魔龙的狂舞)',
'publish_date': datetime.date(2013,10,1),
'price': 330.0,
'currency_id': False,
'rate': 9.5,
'author': 'res.partner,
3',
'publisher': [
1,
2,
3,
4
],
'publish_count': 4,
'__last_update': datetime.datetime(2022,4,1,13,26,2,993416),
'display_name': '冰与火之歌',
'create_uid': (2,
'Administrator'),
'create_date': datetime.datetime(2022,4,1,12,43,56,936257),
'write_uid': (2,
'Administrator'),
'write_date': datetime.datetime(2022,4,1,13,26,2,993416)
}
]
read方法的返回值是一个由字段名和值组成的字典的列表。列表中的每一个字典都是根据当前记录集中的每一条记录生成的,记录中包含了所有传入的字段的值,因此我们可以遍历该结果集然后取出自己想要的结果。
read方法的底层调用的是低阶的_read方法,关于read方法更深入的内容我们将在第五部分介绍有关模型更多的时候继续探索。
_classic_read 模式,具体指的是针对Many2one类型的字段(内部调用了convert_to_read方法,具体参考第十三章),read方法内部获取到字段的值的格式化方式,经典的格式化方法即使用name_get方法将值格式化可读的格式。与之相反的格式化方式,即只返回记录的ID。
read_group方法
read_group方法作用是根据groupby参数对查询的结果进行分组, 它返回一个分组后结果的列表.
先来看方法的定义:
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
pass
它接收如下几个参数:
- domain: 过滤数据的条件 留空则表示获取全部记录
- fields: 要获取的字段列表(只支持数值型,其他类型将被忽略)
- groupby: 分组字段 列表
- offset: 数据偏移
- limit: 返回结果的数量限制
- orderby: 对返回的结果进行排序
- lazy: 布尔值,如果是True,那么只有第一个groupby的字段会生效,且返回值的key会以字段名作为前缀,反之则不会,其他的则存在__context中。如果是False, 所有的groupby将会在一次调用中完成。默认情况下lazy=True.
该方法返回一个包含要求字段和值的字典的列表,eg: [{'field_name_1': value, ...] 。列表中的每一个元素都是根据记录集中的每一条记录生成的。
下面可看一个read_group方法返回的结果:
[{'vendor_id_count': 1, 'price': 5000.0, 'discount': 0.0, 'vendor_id': (9435, <odoo.tools.func.lazy object at 0x7f3efb236f40>), '__domain': ['&', ('vendor_id', '=', 9435), ('product_id', '=', 38751)]}]
如果lazy设置成False,那么返回的结果是:
[{'__count': 1, 'price': 5000.0, 'discount': 0.0, 'vendor_id': (9435, <odoo.tools.func.lazy object at 0x7ffa360dda80>), '__domain': ['&', ('vendor_id', '=', 9435), ('product_id', '=', 38751)]}]
read_group方法与groupby的区别在于read_group方法返回的是分组字段的信息,如果我们想要对结果集进行分组。关于grouby更多内容,参考工具一章
fields字段支持postgresql的聚合函数, 具体参考这个链接
search方法
search方法是odoo中最常用的ORM方法之一,用于检索符合条件的记录。
search方法的定义如下:
def search(self, args, offset=0, limit=None, order=None, count=False):
pass
- args: 是domain,过滤条件
- offset: 偏移量
- limit: 返回结果的限定数量
- order: 排序
- count: 计数
比如我们希望搜索一个书名叫做《海底两万里》的书,那么我们可以这么写搜索语句:
books = self.env["book_store.book"].search([('name','=','海底两万里')])
这里我们搜索出来的是book对象的记录集(recordset)。
排序
假如,我们的书店里有不止一个版本的《海底两万里》,我们希望按照出版日期倒序排列,那么搜索语句就可以这么写:
books = self.env["book_store.book"].search([("name",'=',"海底两万里")],order="date desc")
order默认是正序排列。
limit
假设我们希望返回符合条件的搜索记录中的前两条记录,那么搜索条件应该这么写:
books = self.env["book_store.book"].search([("name",'=',"海底两万里")],order="date desc",limit=2)
name_get方法
name_get方法在所有获取关联对象的名称时被调用,典型的场景就是Many2one字段的搜索框,当我们输入关键字后,下拉里列表中展示出来的名称就是通过name_get方法获取到的。
name_get方法的返回值是一个包含id和名称的元组组成的列表。
使用示例:
def name_get(self):
values = super(Demo, self).name_get()
_logger.info(f"name_get方法返回的结果:{values}")
return values
-------------
[(1,'浓眉大眼的长腿叔叔')]
提到了name_get方法,就不得不提name_search方法,因为我们在Many2one上进行模糊搜索时,搜索部分的工作是由name_search方法完成的,然后name_search把搜索到的结果传递给name_get方法,从而返回我们上面讲到的返回值列表。
name_get从17.0版本起被废弃,直接使用_compute_display_name替代
name_search方法
name_search方法接受4个参数:
- name: 被搜索的关键字
- args: 限定条件domain
- operator: 操作符,可选的参数有:=,!=,>,>=,<,<=,like,ilike,in,not in,child_of,parent_left,parent_right
- limit: 搜索结果的条数,默认100条。
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
pass
私有_name_search方法
其实在更底层的层面上,还有一个_name_search方法,与name_search方法不同的是_name_search方法接受额外的一个参数:name_get_uid,这个参数的作用是指定一个调用_search和lazy_name_get方法的用户ID,用来解决当前用户权限不足的问题。
比如当前我有一个模型osc.person,没有任何一个组有权限访问,正常访问会试如下的界面:
当我们给_name_search方法传一个uid=1进去的时候:
@api.model
def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
_logger.info(f"私有name search方法被调用,参数:{name,args,operator}")
res = super(Person, self)._name_search(name, args, operator, limit, 1)
_logger.info(f"结果:{res}")
return res
name_search方法的底层实现在13.0->14.0时发生了变化,详情请参考附录一版本差异 odoo12中uid=1不再是超级用户的id,变成了机器人odoo_bot,超级管理员的ID变成了2
关于name_search方法的应用,最典型也是最常见的例子就是Many2one字段的查找搜索, 我们在输入关键字以后, 系统会触发onchange事件,然后调用name_search来搜索匹配的结果, 最后使用name_get方法将格式化后的结果组织成下拉列表的形式展示出来.
这是提一个有关于name_seach方法的扩展应用, 默认情况下, name_search只能针对于模型的name字段进行搜索, 而不能搜索本模型的其他字段, 这在某些条件下限制了我们的搜索可能行.因此, 笔者开发了name_search_options这个模块专门来处理这个问题.
fields_get_keys
获取本模型的所有字段,其原理即返回当前模型对对象的_fields属性列表。
@api.model
def fields_get_keys(self):
return list(self._fields)
load_views
load_views方法用来加载当前模型指定的视图,并且可以选择性的指定动作的过滤器。
@api.model
def load_views(self, views, options=None):
pass
它接收两个参数views和options。
- views: 由视图ID和视图类型组成的列表,[(view_id, view_type)]
- options: 参数字典
- toolbar: 为真时加载上下文工具
- load_filters: 为真时返回模型的过滤器
- action_id: 为获取过滤器的动作ID
返回值为包含fields_views,fields和filters的字典。load_views方法在每次浏览器加载视图时都会被调用,一般不需要重载,除非你对返回的视图有特殊的需求。
下面是load_views返回值的一个示例:
fields_view_get
fields_view_get方法是用于获取视图的详细组成的方法。它跟load_views的关系是,load_view方法内部调用了本方法获取到详细的视图类型和视图布局。
fields_view_get方法的定义如下:
@api.model
def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
pass
接收的参数如下:
- view_id: 视图的ID或None
- view_type: 视图的类型(form,tree, ...)
- toolbar: 是否包含上下文动作
- submenu: 该参数已过时弃用
比如我们之前的book_store应用,在加载视图的过程中,调用fields_view_get方法返回的结果示例如下:
{'model': 'book_store.book', 'field_parent': False, 'arch':
'<search string="图书搜索">\n <field name="author" can_create="true" can_write="true" modifiers="{"required": true}"/>\n <field name="date" modifiers="{}"/>\n <field name="price" modifiers="{}"/>\n <separator/>\n <filter name="liu_book" string="大刘小说" domain="[(\'author\',\'=\',\'刘慈欣\')]"/>\n <separator/>\n <group expand="0" string="Group By">\n <filter name="author" string="按作者分组" domain="[]" context="{\'group_by\':\'author\'}"/>\n </group>\n </search>',
'name': '图书搜索', 'type': 'search', 'view_id': 813, 'base_model': 'book_store.book',
'fields': {'author': {'type': 'many2one', 'change_default': False, 'company_dependent': False, 'context': {}, 'depends': (), 'domain': [], 'help': '作者', 'manual': False, 'readonly': False, 'relation': 'book_store.author', 'required': True, 'searchable': True, 'sortable': True, 'store': True, 'string': '作者', 'views': {}},
'date': {'type': 'date', 'change_default': False, 'company_dependent': False, 'depends': (), 'help': '日期', 'manual': False, 'readonly': False, 'required': False, 'searchable': True, 'sortable': True, 'store': True, 'string': '出版日期', 'views': {}},
'price': {'type': 'float', 'change_default': False, 'company_dependent': False, 'depends': (), 'group_operator': 'sum', 'help': '定价', 'manual': False, 'readonly': False, 'required': False, 'searchable': True, 'sortable': True, 'store': True, 'string': '定价', 'views': {}}}}
很容易看出,这个加载的是搜索的视图,同样的,form和tree视图加载时也会调用同样的方法。我们知道,odoo的页面布局都是写在XML中然后静态存储在数据库中的,而fields_view_get就给了我们一个动态修改视图的机会,我们可以根据自己需要在拿到视图数据之后进行修改,然后再返回给前端。
动态修改XML我们常用到的库是lxml, 通常的修改步骤是:
- 使用fromstring方法加载xml文本
- 使用xpath定位到要修改的节点
- 修改视图结构
- 将修改后的节点重新导出成文本供fields_view_get方法使用
fields_view_get方法只在第一次加载的时候被调用, 之后的操作不会再次触发此操作.
下面给出一个修改视图的示例:
@api.model
def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
result = super(AssetModify, self).fields_view_get(view_id, view_type, toolbar=toolbar, submenu=submenu)
asset_id = self.env.context.get('active_id')
active_model = self.env.context.get('active_model')
if active_model == 'account.asset.asset' and asset_id:
asset = self.env['account.asset.asset'].browse(asset_id)
doc = etree.XML(result['arch'])
if asset.method_time == 'number' and doc.xpath("//field[@name='method_end']"):
node = doc.xpath("//field[@name='method_end']")[0]
node.set('invisible', '1')
setup_modifiers(node, result['fields']['method_end'])
elif asset.method_time == 'end' and doc.xpath("//field[@name='method_number']"):
node = doc.xpath("//field[@name='method_number']")[0]
node.set('invisible', '1')
setup_modifiers(node, result['fields']['method_number'])
result['arch'] = etree.tostring(doc, encoding='unicode')
return result
16.0起本方法被废除, 使用get_view方法替代.
fields_get方法
fields_get方法返回每个字段的定义
@api.model
def fields_get(self, allfields=None, attributes=None):
pass
它接收两个参数:
- allfields: 字段列表,如果为空则返回全部字段
- attributes: 每个字段的属性描述,如果为空则返回全部属性
前面我们知道,fields_view_get是被load_views调用的,同样的,fields_get也是被load_views调用的。他们一个返回视图的结构,一个返回字段的描述,最终形成一个完整的视图。我们可以看一下fields_get返回的结果示例:
{'name':
{'type': 'char', 'change_default': False, 'company_dependent': False, 'depends': (), 'help': '书名', 'manual': False, 'readonly': False, 'required': False, 'searchable': True, 'sortable': True, 'store': True, 'string': '名称', 'translate': False, 'trim': True},
'author': {'type': 'many2one', 'change_default': False, 'company_dependent': False, 'context': {}, 'depends': (), 'domain': [], 'help': '作者', 'manual': False, 'readonly': False, 'relation': 'book_store.author', 'required': True, 'searchable': True, 'sortable': True, 'store': True, 'string': '作者'}, 'date': {'type': 'date', 'change_default': False, 'company_dependent': False, 'depends': (), 'help': '日期', 'manual': False, 'readonly': False, 'required': False, 'searchable': True, 'sortable': True, 'store': True, 'string': '出版日期'},
'price': {'type': 'float', 'change_default': False, 'company_dependent': False, 'depends': (), 'group_operator': 'sum', 'help': '定价', 'manual': False, 'readonly': False, 'required': False, 'searchable': True, 'sortable': True, 'store': True, 'string': '定价'}, 'ref': {'type': 'reference', 'change_default': False, 'company_dependent': False, 'depends': (), 'manual': False, 'readonly': False, 'required': False, 'searchable': True, 'selection': [('book_store.author', '作者'), ('book_store.publisher', '出版商')], 'sortable': True, 'store': True, 'string': 'Ref'}, 'age': {'type': 'integer', 'change_default': False, 'company_dependent': False, 'depends': ('date',),
'group_operator': 'sum', 'manual': False, 'readonly': True, 'required': False, 'searchable': True, 'sortable': False, 'store': False, 'string': '书龄'}, 'category': {'type': 'char', 'change_default': False, 'company_dependent': False,
'depends': (), 'manual': False, 'readonly': False, 'required': False, 'searchable': True, 'sortable': True, 'store': True, 'string': '分类', 'translate': False, 'trim': True}, 'id': {'type': 'integer', 'change_default': False, 'company_dependent': False, 'depends': (), 'manual': False, 'readonly': True, 'required': False, 'searchable': True, 'sortable': True,
'store': True, 'string': 'ID'}, 'display_name': {'type': 'char', 'change_default': False, 'company_dependent': False, 'depends': (), 'manual': False, 'readonly': True, 'required': False, 'searchable': False, 'sortable': False, 'store': False, 'string': 'Display Name', 'translate': False, 'trim': True}, 'create_uid': {'type': 'many2one', 'change_default': False, 'company_dependent': False, 'context': {}, 'depends': (), 'domain': [], 'manual': False, 'readonly': True, 'relation': 'res.users', 'required': False,
'searchable': True, 'sortable': True, 'store': True, 'string': 'Created by'},
'create_date': {'type': 'datetime', 'change_default': False, 'company_dependent': False, 'depends': (), 'manual': False, 'readonly': True, 'required': False, 'searchable': True, 'sortable': True, 'store': True, 'string': 'Created on'}, 'write_uid': {'type': 'many2one', 'change_default': False, 'company_dependent': False, 'context': {}, 'depends': (), 'domain': [], 'manual': False, 'readonly': True, 'relation': 'res.users', 'required': False, 'searchable': True, 'sortable': True,
'store': True, 'string': 'Last Updated by'}, 'write_date': {'type': 'datetime', 'change_default': False, 'company_dependent': False, 'depends': (), 'manual': False, 'readonly': True, 'required': False, 'searchable': True, 'sortable': True, 'store': True, 'string': 'Last Updated on'}, '__last_update': {'type': 'datetime', 'change_default': False, 'company_dependent': False, 'depends': ('create_date', 'write_date'), 'manual': False, 'readonly': True, 'required': False, 'searchable': False,
'sortable': False, 'store': False, 'string': 'Last Modified on'}, 'publisher_id': {'type': 'many2one', 'change_default': False, 'company_dependent': False, 'context': {},
'depends': ('author.publisher_id',), 'domain': [], 'help': '', 'manual': False,
'readonly': False, 'related': ('author', 'publisher_id'), 'relation': 'book_store.publisher',
'required': True, 'searchable': True, 'sortable': True, 'store': False, 'string': '签约出版商'}}
fields_get方法默认返回所有的字段和属性, 但这里有个隐藏的因素, 如果方法的调用者没有该字段的访问权限, 那么返回的结果中将不包含该字段.
mapped方法
mapped方法提供了一种简洁地获取数据集(recordset)的方法,官方定义:
mapped(): applies the provided function to each record in the recordset, returns a recordset if the results are recordsets。The provided function can be a string to get field values.
说人话呢,就是mapped方法返回一个记录集合,需要传入的参数是对象的一个字段。
比如,我们都知道,销售订单(sale.order)对象有一个明细的one2many的字段order_line,假设我想获取order_line中单价大于1的记录的prouduct_id的列表
传统的写法:
lines = order.order_line.filtered(lambda l:l.price_unit > 1)._ids)
products = [line.product_id for line in lines]
使用mapped方法:
products = order.order_line.filtered(lambda l:l.price_unit > 1).mapped( "product_id" )
显然,使用mapped方法要比传统写法方便了很多。
关于mapped方法更多的内容,请参考第五部分模型字段一章。
sorted方法
sorted方法提供了对数据集排序的一种快捷方式。比如,我们有一列采购单purchase.order.line(1192, 1193, 1194),出于某种特定需求,我们希望将其倒序排列,那么就可以使用sorted方法。
purchase_order_lines.sorted(reverse=True)
sorted方法接收两个参数:
- key: 用来进行比较的字段,也可以是一个接收一个参数的函数,它返回每条记录的用来比较的关键字。
- reverse: 是否进行倒序排列。
例子:
records.sorted(key=lambda r: r.name)
search_read方法
search_read方法提供了一个先搜索再读取的快捷方法。
@api.model
def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None):
pass
参数:
- domain:search方法运行时的domain
- fields:read方法运行时的字段列表
- offset:搜索结果的的偏移值
- limit:返回结果的限制
- order:排序结果,默认不排序
返回一个字典的列表。
search_read方法很常见的一个使用场景就是在jsonrpc中,例如,我们希望在前端中获取某个省份的值,就可以这么写:
this._rpc({
model: "res.country.state",
method: "search_read",
domain: [['name','=','北京市']],
fields: ['name']
})
返回结果:
result: [{id: 747, name: "北京市"}]
concat方法
concat方法用来连结相同对象的多个记录, 例如现在两条记录 res.users(1,) 和 res.users(2,), 利用concat方法,我们就可以得到一个包含1,2两条记录的集合
r1 = res.users(1,)
r2 = res.users(2,)
r3 = r1.concat(r2)
=======
res.users(1,2)
default_get方法
default_get方法用来获取传入的字段的默认值,它的使用场景是需要同时设置多个字段时使用。如果针对单个字段的设置,可以使用字段属性的default属性进行设置。
它接收一个参数fields_list, 返回一个由相应字段及其默认值组成的字典.
res = self.default_get()
=======================
{
"field_1": a,
"feild_2": b,
...
}
当字段传入到default_get方法后, odoo会依次从以下几个地方寻找默认值:
- 环境变量的上下文中
- ir_defaults, 也就是模型定义时指定的默认值.
- 字段定义时的默认值
- 继承的父类的默认值
找到即返回,不会再继续寻找。
对于X2many类型的字段,我们在进行默认值设置的时候,使用的直接的ids赋值,而不是使用命令字(4,6,...)进行赋值。
default_get方法的运行时机是在create方法内,通过_prepare_create_values方法内的_add_missing_default_values调用完成的。也就是说,我们在使用create方法的vals内是获取不到默认值的,因为这个时候,default_get方法尚未被调用。
对于company_dependent类型的字段,其默认值的获取过程还有些曲折,详情参考第五部分默认值一章。
copy方法
copy方法用来复制当前记录的值,如果某些字段不想要被复制,那么设置其copy属性为False
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
""" copy(default=None)
Duplicate record ``self`` updating it with default values
:param dict default: dictionary of field values to override in the
original values of the copied record, e.g: ``{'field_name': overridden_value, ...}``
:returns: new record
"""
self.ensure_one()
vals = self.with_context(active_test=False).copy_data(default)[0]
# To avoid to create a translation in the lang of the user, copy_translation will do it
new = self.with_context(lang=None).create(vals).with_env(self.env)
self.with_context(from_copy_translation=True).copy_translations(new, excluded=default or ())
return new
从copy方法的定义上,我们可以看得出
- copy方法只能在单个记录上使用
- copy方法先是使用无语言环境的记录复制,然后再将原有的翻译值复制过来。
get_formview_action方法
get_formview_action方法用来返回打开一个模型的form表单动作。get_formview_action接收一个参数access_uid,用来指定要跳转的用户id。
例如,我们在销售订单中打开客户联系人的表单就是利用了get_formview_action方法。
15.0及早期版本get_formview_action方法是在models.py文件中的,16.0开始移到了ir_ui_view.py文件中。
借助get_formview_action方法可以帮我们实现一些权限控制方面的校验。
总结
本章介绍了ORM中比较常用的几个方法,随着学习的深入,我们会逐渐了解每个方法及其适用的场景。接下来,我们将从后台的角度去理解模型的概念。