ORM应用逻辑-业务处理
- 前面的章节我们学习了利于Odoo的视图来构建用户前端界面。本章介绍Odoo的后台业务逻辑实现。
创建一个向导
- 假设有这么一个需求:To-Do应用中,用户需要设置一系列任务的截止时间跟负责人。如果单独的打开每条任务记录去修改那是十分麻烦的。我们就需要使用一个向导表单来选择需要更改的任务记录,再进行统一操作。
- 向导表单可以理解为获取用户输入,然后再对这些输入进行储存,以便下一步应用到Odoo模型记录中。
- 我们来新建一个名为 todo_wizard 的模块用以演示。
- 还是跟往常一样。创立
todo_wizard/__manifest__.py
文件添加如下代码来进行模块的描述.
- 还是跟往常一样。创立
{
'name': 'To-do Tasks Management Assistant',
'description': 'Mass edit your To-Do backlog.',
'author': 'Xer',
'depends': ['todo_user'],
'data': ['views/todo_wizard_view.xml'],
}
别忘记创建todo_wizard/__init__.py
.进行导包操作
from . import models
- 接下来,我们来对向导的数据支持模型进行介绍.
向导模型
- 一个向导展示了表单视图给用户。通常作为一个对话窗口,上面展示了在向导逻辑中需要展示的字段。
- 这与我们定义普通模型十分相似,唯一不同的就是我们使用 models.TransientModel 来代替 models.Model 基类.
- 临时模型是用来提高效率的,临时模型在数据库中也有对应的数据库表结构. 使用向导模型时,数据库中存储最新的使用数据.每次都会对原有的数据进行覆盖. 这样就不会产生多余的无效数据.
- 创建 models/todo_wizard_model.py 文件,添加代码:
class TodoWizard(models.TransientModel):
_name = 'todo.wizard'
_description = 'To-do Mass Assignment'
task_ids = fields.Many2many('todo.task', string='Tasks')
new_deadline = fields.Date('Deadline to Set')
new_user_id = fields.Many2one('res.users', string='Responsible to Set')
注:别忘记添加__init__.py
.加入代码from . import todo_wizard_model
- 在临时模型中,不要使用 one-to-many 关系型字段.原因 在与普通模型与临时模型创建 many-to-one 关系需要垃圾回收的支持。这一般都不被允许。
向导表单
- 与普通视图类似,唯一不同是有两个特殊元素:
-
: 可以用来存放动作按钮
-
type="cancel"
: 按钮中使用的属性,可以取消向导表单的数据展示。
-
- 编辑视图xml文件。views/todo_wizard_view.xml
To-do Task Wizard
todo.wizard
这个动作把打开向导视图添加到todo.task表单视图中的More 按钮中.设置target="new"
能够打开一个新的窗口.
另外在确定按钮Mass Update 设置了额外的属性,只有选择了新的截止日期或者新的任务负责人才会显示这个按钮。
向导的业务逻辑
- 我们现在来实现定义在向导视图中的3个动作的逻辑。
- 首先我们来解决Mass Update 按钮
在todo_wizard_model.py
文件中定义do_mass_update
函数。
@api.multi
def do_mass_update(self):
self.ensure_one()
if not (self.new_deadline or self.new_user_id):
raise exceptions.ValidationError('No data to update!')
_logger.debug('Mass update on Todo Tasks %s', self.task_ids.ids)
vals = {}
if self.new_deadline:
vals['date_deadline'] = self.new_deadline
if self.new_user_id:
vals['user_id'] = self.new_user_id
if vals:
self.task_ids.write(vals)
return True
我们的代码只需要对向导的一个实例进行处理,使用
self.ensure_one() 来保证是单例.
处理逻辑:
- 对向导中的新截止日期个么新任务负责人取值.如果两者不存在,即没有设置,此时点击 Mass Update 返回一个类型错误.
- 构造一个名为
vals
的字典,如果有新的设置,保存到字典,然后对向导中选择的所有任务进行字段更新.
注意到write
方法可以对一组recordset进行写入操作.
日志
- 我们在使用向导功能批量修改任务时可能会有误操作,这时候就需要使用日志文件来对操作进行记录.
- Odoo中,我们直接使用了python的自带
logging
标准库来进行日志的操作.- 可执行的日志记录:
import logging
_logger = logging.getLogger(__name__)
_logger.debug('A DEBUG message')
_logger.info('An INFO message')
_logger.warning('A WARNING message')
_logger.error('An ERROR message')
异常处理
- 当程序运行出错时,我们希望暂停它,然后打印出出错信息。通过抛异常来进行此类操作。
Odoo中定义了的异常类:
from odoo import exceptions
raise exceptions.Warning('Warning message')
raise exceptions.ValidationError('Not valid message')
Odoo中,我们可以使用抛出警告类来实现用户界面的弹出窗口。编辑Count按钮的逻辑
@api.multi
def do_count_tasks(self):
Task = self.env['todo.task']
count = Task.search_count([('is_done', '=', False)])
raise exceptions.Warning(
'There are %d active tasks.' %count
)
向导视图中的帮助动作
- 我们来编写一个 Get All 按钮。它的功能在于点击后能够选取所有active属性为true的任务。
- 这里有个小细节,在对话窗口中,一个按钮上的动作执行成功后会关闭对话窗口。(可能会说,上一个 Count 按钮没有这个问题,那是因为我们使用抛出异常来传递了任务数,本质上这个动作没有成功执行,自然不存在关闭对话窗口这个问题)。
- 我们通过重新打开向导视图来解决这个细节问题。来定义这个重新打开功能
@api.multi
def _reopen_form(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'res_id': self.id,
'view_type': 'form',
'view_mode': 'form',
'target': 'new'
}
- 编写 Get All 按钮逻辑
@api.multi
def do_populate_tasks(self):
self.ensure_one()
Task = self.env['todo.task']
open_tasks = Task.search([('is_done', '=', False)])
self.task_ids = open_tasks
return self._reopen_form()
使用ORM API
- 接下来我们来深入Odoo中的ORM API 以便更好的操作模型。
装饰器
- 在这么多章节里,我们经常能看到类似于
@api.multi. 这样的形式的装饰器。下面来对这些形式的装饰器进行解释 - @api.multi :这个装饰器是Odoo10中新的API,用来处理recordsets(数据集合)。在这个装饰器中,self代表了一个数据集合。我们用它装饰的方法经常会首先对self进行遍历以获取每一个record。
- @api.one : 这个装饰器是在Odoo9.0版本中添加的.目前版本中我们应该减少它的使用.通过@api.multi 装饰器,使用
self.ensure_one()
来替代. - @api.model : 这个装饰器装饰了一个静态类方法。它不涉及到任何recordset数据。虽然方法中的
self
还是一个recordset,但是里面的内容是不相关的.被这个装饰器装饰的方法无法在按钮上注册使用. - @api.depends : 用于计算字段.
- @api.constrains : 用以限制字段.
- @api.onchange : onchange机制用来触发字段值的变动. onchage装饰器还能在用户界面返回一条警告信息。在return 中定义下面的代码即可:
return {
'warning' : {
'title' : 'Warning!',
'message' : 'You have benn warned'}
}
重写ORM的默认方法
- 比较常见的是我们重写 create 跟 write 方法.我们可以把我们的代码逻辑加入到这两个方法中,这样在记录被创建或者修改时就能执行这些代码逻辑.
- 还是通过 TodoTask 来举例,我们可以对create方法进行扩展.
@api.model
def create(self, vals):
# Code before create: can use the `vals` dict
new_record = super(TodoTask, self).create(vals)
# Code after create: can use the `new_record` created
return new_record
- 对write方法进行扩展:
@api.model
def write(self, vals):
# Code before write: can use the `self` dict, with the old vals
super(TodoTask, self).create(vals)
# Code after write: can use `self`, with the updated vals
return True
- Todo task适用的一些常用的技术方法:
- 使用计算字段来从一个基础字段中获得更进一步的信息
- 使用default方法来动态展示字段默认值
- 使用on change 来监控字段的改变
- 使用constraints装饰器来限制字段。
RPC,网页前端相关的方法。
-
下面这些方法一般可以用来作为特殊的视图动作的基础。
- read([fields])
: 与 browse 方法相同,但是不是返回一个记录集合,而是一个以字段为参数的所有记录的列表。这个方法常用作于序列化数据。通常在客户端使用。 - search_read([domain], [fields], offset=0, limit=None, order=None) :与read方法相似,添加了搜索功能。
- load([fields], [data]) : 在从csv导入数据时使用,前一个fiedls参数代表要导入的字段名.
- export_data([fiedls], raw_data=False) : 网页客户端上的导出功能.返回一个包含数据的字典. raw_data 参数允许导出为Python类型的数据值而不用转换为string.
- read([fields])
-
下面的方法常用来网页客户端,为用户界面的展示作为基础。
- name_get() : 返回了一个(ID, name)元组格式的列表。这个方法用来定义作为用户界面展示的模型默认名字的字段。
- name_search(name=' ', args=None, operator='ilike',limit=100) : 同样返回了一个(ID, name)元组格式的列表。不过需要跟参数中的name值相同,可以看做上一个方法的搜索形式。
- name_create(name) : 这个是为关联字段中为关联模型快速创建记录时设置一个记录名字.
- default_get([fields]) : 当一个新纪录被创建时,返回一个包含字段默认值的字典。里面的默认值依赖当前用户或者当前会话中的上下文。
fiedls_get() : 描述当前模型的默认字段定义.能够通过开发者模式中的 View Fields 选项来查看.
fields_view_get() : 可以认为是获取视图显示模式。
Shell 命令
- Odoo提供了一个交互式对话的命令行界面来让我们更好的理解它的ORM API。
- 使用
./odoo-bin shell -d todo
来进入到我们的Odoo shell界面。 - 进入shell界面后。我们发现会跟python解释器的shell一样,出现了 >>> 这样的等待输入标识. 我们输入
self
.可以从返回的信息中得出现在的self 代表了我们是管理员用户.
服务器环境
刚才的shell界面提供的self 关联了res.users
这个用户模型.我们知道,在Odoo中self
一般代表了一个记录集。而记录集通常携带着当前的上下文环境信息。可以通过self.env
来获取当前的上下文环境.
- 当前用户的环境信息拥有以下属性:
- env.cr : 目前使用的数据库游标
- env.uid : 当前会话的用户ID
- env.user : 当前用户
- env.context 当前会话的上下文字典数据.
我们还可以通过env从Odoo模型登记处的获取已经安装的Odoo模型。例如self.env['res.partner']
返回了Partners模型.我们可以再使用search跟browse方法来获取模型中的记录集。
改变环境的执行状态
- 环境是无法更改的,但是我们能够创立一个修改过的环境,然后通过切换环境执行相应的动作。
- 改变环境可以使用的方法:
- env.sudo(user) : 可以通过传入的用户名来切换当前的用户使用环境。如果user不设置,默认为切换到管理员用户环境。
- env.with_context(dictionary) : 使用新的字典数据来替换原来的context.
- env.with_context(key=value,...) : 使用新的键值对代替了当前context中已经存在的。
- env.ref() 使用了外部id来获取对于的记录.举例:
事务及底层SQL
- 通常数据写入操作都由已经定义好的SQL事务来处理。在某些情况下,我们需要对数据库执行更加完善的控制,就可以使用
self.env.cr
.- self.env.cr.commit() : 提交事务缓存区中的执行命令
- self.env.savepoint() : 设置一个回滚点
- self.env.rollback() : 回滚数据库数据到回滚点.
- 使用游标类cr的execute()方法,我们可以直接使用SQL语句对Odoo数据库进行操作.
注意点:在直接使用SQL语句时,不要直接写死SQL语句,而是通过python的字符串代替符号%s来进行值的传入.这样能有效防止SQL注入攻击. - 在execute()方法中使用查询语句,这是就需要用fetchall()来获取查询记录. fetchall()方法返回记录的元组列表,使用 dictfetchall() 可以返回字典列表.
- 使用改变数据库结构语句(DML)时,例如UPDATE , INSERT。需要对缓存进行更新。使用
self.env.invalidate_all()
方法执行.
使用记录集
下面我们来扩展ORM的常用方法的实现。首先使用shell 命令来交互式的进行记录集(recordsets)的讲解。
查询模型
- 通过
self.env
我们能够获取到Odoo中已安装的模型. 得到模型后可以使用 search() 方法来对记录集进行搜索.- search() : 搜索方法需要传入一个domain表达式.如果传入
domain = [ ]
.就会返回所有的记录.另外要注意, 如果使用了active字段, 只有active=True
的记录会被获取.另外一些参数如下- order : 排序规则,通常使用字段名字排序
- limit : 设置搜索获取的记录个数的最大值。
- offset : 偏移量,可以与limit一起使用来获取返回记录集中的特定记录段。
- search() : 搜索方法需要传入一个domain表达式.如果传入
- 有时候我们只需要获取满足条件的记录的个数。这时候我们使用 search_count() 方法就可以。
- browse() : 这个方法通过传入ID列表或者特定的ID值来获取与之对应的记录集合或者单条记录。
举例:
单例
- 只有一条记录的记录集我们成为单例记录集。单例记录集可以直接使用
.
操作符来获取记录的字段值。例:
记录集有个 ensure_one() 方法可以来确认当前记录集是否为单例,如果记录集里有多条记录,这个方法就会抛出异常
记录的写入操作。
- 我们获取到记录集中的记录后可以直接对其字段属性进行修改。这些修改会直接被写入到数据库中。
- 记录集同样有3个方法来对数据进行操作
- create() : 可以直接通过字典数据创建一个新的记录.
- unlink() : 删除记录
- write() : 更新记录的字段
- copy() :复制一个已有的记录。注意,字段有
copy=False
属性的话不会被复制
使用时间跟日期
- 由于历史遗留问题,ORM记录集使用string来处理
date
跟datetime
的值.它们被分别存储在数据库的两张表中.- odoo.tools.DEFAULT_SERVER_DATE_FORMAT
- odoo.tools.DEFAULT_SERVER_DATETIME_FORMAT
它们的格式 %Y-%m-%d 跟 %Y-%m-%d %H:%M:%S
- date跟datetime在服务端使用UTC格式存储。实际使用过程中时区可能会有一些不同。可以使用下面的方法进行时区的处理:
- fields.Date.today() :返回当前的日期字符串
- fields.Datetime.now() : 返回当前的日期时间
- fields.Date.context_today(record, timestamp=None) : 通过会话上下文中传入的时区的值来返回日期
- fields.Datetime.context_timestamp(record, timestamp) : 根据时区转换一个当前的日期时间,时区的取值从会话上下文中获取。
- Date跟Datetime字段对象都有2个方法用来与string进行互相转换。分别是 from_string(value) 跟 to_string(value) .
记录集操作
- in 操作: 判断一条记录是否在记录集中
- recordset.ids :返回记录集中记录的ID列表
- reocrdset.ensure_one() : 确认是否只包含单条记录。
- recordset.filtered(func) : 使用func方法作为过滤,返回过滤记录集
- recordset.mapped(func) : 理解为python中的map方法.返回map操作后的数据集
- recordset.sorted(func) : 使用func方法进行排序.返回排序后的数据集.
下面是一些例子用来理解这些方法:
操纵数据集
- 数据集中的数据是不可变的,就跟python中的str,int,tuple一样。所以对数据集的增加,取代,删除动作实际上是生成新的数据集。
数据集的操作跟集合一样有四种操作方式。 - rs1 | rs2 :并操作。
- rs1 + rs2 : 加操作。这个操作可能会产生重复的记录数据。
- rs1 & rs2 : 交(intersection)操作。返回两个记录集中同时拥有的元素构成的记录集。
- rs1 - rs2: 差(difference)操作,取rs1中存在而rs2中不存在的元素的记录集。
- 分片操作也可以使用:
rs[0] : 取第一个
rs[-1] : 最后一个
另外的操作: - self.task_ids | = task1 : 添加task1这条记录。
- self.task_ids -= task1 : 删除task1这条记录。
- *self.task_ids = self.task_ids[:-1] : 删除最后一条记录。
对于关系型字段。还有一种方式来进行记录集的操作。使用create()跟write()方法。使用类似在XML文件定义关联字段值的语法。
例 - self.write([(4, task1.id, None)]) : 添加task1记录
- self.write([(3, task1.id, None)]) : 删除task1记录。
- self.write([(3, self.task_ids[-1].id, False)]) : 删除最后一条记录。
使用关系型字段
- 如前面一开始所说的,我们可以使用
.
操作符来获取到单例记录的字段值。 - 在many-to-one关系中。我们可以直接用
.
操作符直接获取到关联模型的所有记录。
- 更为方便的是,空的记录集也会被看做一个单例记录集,使用
.
操作符来操作空记录集会得到False而不会抛出异常。