本章我们更进一步学习模型层,以及如何使用模型来设计支撑应用的数据结构。我们会探讨可用的模型类型,以及在使用这些类型时如何定义强制进行数据验证的约束。
模型由支持不同数据类型的数据字段组成,一些字段类型支持定义模型间的关联。对于字段更高级的使用包含使用具体的业务逻辑自动计算的值。
本文的主要内容有:
- 学习项目 - 优化图书应用
- 创建模型
- 创建字段
- 模型间的关联
- 计算字段
- 模型约束
- Odoo的 base 模型总览
通过这些内容,我们将学习如何为Odoo项目创建大型数据结构。在学完本章后,读者对于架构数据模型相关的功能应该会有清晰的认知。
开发准备
本文代码基于第三章 Odoo 15开发之创建第一个 Odoo 应用中所创建的代码。相关代码参见 GitHub 仓库的ch06/目录。
请将其添加至插件路径中,并安装好library_app模块。
学习项目 - 优化图书应用
在第三章 Odoo 15开发之创建第一个 Odoo 应用中,我们创建了一个library_app插件模块,实现了一个简单的library.book模型用于展示图书目录。本章中,我们将回到该模块来丰富存储的图书数据。
我们使用如下结构添加一个分类层级,用于图书分类:
- Name:分类标题
- Parent:所属父级分类
- Subcategories:将此作为父级分类的子分类
- Featured book或author: 此分类中所选图书或作者
我们会添加一些字段来展示 Odoo中字段的数据类型。我们还会为图书模型添加一些验证约束:
- 标题和出版日期应唯一
- 输入的ISBN应为有效
下面我们就更深度地学习Odoo模型,学习可以使用的所有选项。
创建模型
模型是 Odoo 框架的核心。它们描述应用的数据结构,是服务端应用和数据库存储之间的桥梁。可围绕模型实现业务逻辑来为应用添加功能,用户界面所提供的用户体验也建立在模型之上。
下面几节我们将学习模型的通用属性,用于影响行为,以及几种模型类型:普通(regular)模型、临时(transient)模型和抽象(abstract)模型。
模型属性
模型类可以使用控制行为的一些其它属性。以下是最常用的属性:
- _name:它是我们创建的 Odoo 模型的内部标识符,新建模型时为必填。
- _description:它是对用户友好的标题,指向单个模型记录,如Book。可选但推荐添加。如未设置,在加载过程中服务端日志会显示警告。
- _order:设置浏览模型记录时或列表视图的默认排序。其值为 SQL 语句中 order by 使用的字符串,所以可以传入符合 SQL 语法的任意值,它有智能模式并支持可翻译字段及多对一字段名。
我们的图书模型中已使用了_name 和_description属性。可以添加一个_order属性来默认以图书名排序,然后按出版日期倒序排(新出版在前)。
class Book(models.Model):
_name = 'library.book'
_description = 'Book'
_order = 'name, date_published desc'
在高级用例中还会用到如下属性:
- _rec_name:设置记录显示名的字段。默认为name字段,因此我们通常选择它作为记录标题。
- _table:是模型对应的数据表名。通常由ORM自动设置,替换模型名称中的点为下划线,但是我们可以通过该属性指定表名。
- _log_access=False:用于设置不自动创建审计追踪字段,也即create_uid、create_date、write_uid和write_date。
- _auto=False:用于设置不自动创建模型对应的数据表。这时应使用init()方法来编写创建数据库对象、数据表或视图的具体逻辑。通常用于支持只读报表的视图。
例如,以下代码为 library.book模型设置了一些默认值:
_recname = "name"
_table = "library_book"
_log_access = True
_auto = True
注:还有用于继承模块的_inherit和_inherits属性,在本文后续会深入学习。在第四章 Odoo 15开发之模块继承中已详细讲解过。
在使用_auto = False时,我们是在重载创建数据库对象的过程,所以应编写相应逻辑。常用的场景是报表 ,基于收集报表所需数据的数据库视图。
以下是从sale内核模块中抽取的示例,来自sale/report/sale_report.py文件:
def init(self):
tools.drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute(
"CREATE or REPLACE VIEW %s as (%s)"
% (self._table, self._query())
)
以上代码使用了Python模块tools,需要使用from odoo import tools
导入。
模型和 Python 类
Odoo 模型使用Python类,在前面的代码中,有一个继承了 models.Model类的 Python 类:Book,用于定义名为library.book的Odoo模型。
Odoo的模型保存在中央注册表(central registry)中,可通过环境对象获取,通常为self.env。中央注册表保存对所有模型的引用,过使用类字典语法访问。
例如,可在方法内使用self.env['library.book']或self.env.get(["library.book"])来获对图书模型的引用。
可以看出模型名非常重要,它是访问模型注册表的关键。
模型名必须全局唯一。因此使用模块所属应用的每个单词作为模型名称的第一个单词是一种良好实践。就Library应用而言,所有的模型名应使用library前缀。内核模型的其它例子有project、crm或sale。
小贴士:模型名应使用单数形式library.book,不应使用library.books。规范是以点号连接一组小写的单词。第一个单词标识所属主应用,如library.book或library.book.category。例如官方插件中有project.project、project.task和project.task.type。
此外,Python类名为Python文件本地所有,声明与Odoo框架没有关联。所用的名称仅在该文件中具有意义,关联度不高。Python有关类名按照PEP8的规范是使用驼峰命名(CamelCase)。
存在几种模型类型。最常用的一种是models.Model类,用于数据库持久化存储模型。接下来我们学习其它几种模型类型。
临时(Transient)模型和抽象模型
大多数据 Odoo 模型中的类会继承models.Model类。这类模型在数据库中持久化存储,会为模型创建数据表并存储记录直至删除。一般使用它就够了。
但有时我们并不需要持久化的数据库存储,这时就可以使用下面两类模型:
- 临时模型继承models.TransientModel类,用于向导式的用户交互。这类数据会存储在数据库中,但仅是临时性的。会定时运行清空 job 来清除这些表中的老数据。比如Settings > Translations > Import Translation 菜单打开一个对框窗口,使用临时模型存储用户选项并实现向导逻辑。在第八章 Odoo 15开发之业务逻辑 - 业务流程的支持中会有讨论临时模型的示例。
- 抽象模型继承models.AbstractModel类,它不含数据存储。抽象模型用作可复用的功能集,与使用 Odoo 继承功能的其它模型相配合。例如mail.thread是 Discuss 应用中的一个抽象模型,用于为其它模型添加消息和关注功能。使用抽象模型的 mixin 类以及前述的 mail.thread示例在第四章 Odoo 15开发之模块继承中进行了讨论。
查看已有模型
通过 Python 类创建的模型和字段可通过用户界面查看。启用开发者模式,访问菜单Settings > Technical > Database Structure > Models,这里有数据库中的所有模型。
点击列表中的模型会打开详情表单,如下图所示:
图6.1:通过Technical菜单查看图书模型
这是一个查看模型结构很好的工具,因为在这里可以看到不同模块所有的修改结果。表单视图右上角 In Apps字段中可以看到对其施加影响的模块列表。本例中library.book模型受library_app和library_member两个模块的影响。
小贴士:如第一章 使用开发者模式快速入门 Odoo 15中所见,模型表单是可编辑的。通过这里是可以创建并修改模型、字段和视图的。可在此处创建原型然后在插件模块中实现。
下方区域中还有几个包含其它信息的标签:
- Fields显示模型的字段
- Access Rights显示授予不同权限组的访问控制规则
- Record Rules显示用于记录过滤器的记录规则
- Notes是模型定义文档字符串
- Views显示模型所带的视图
我们可以通过开发者菜单下的View Metadata选项查看模型的外部标识符或XML ID。模型的外部标识符由 ORM 自动生成,遵循一套简单规则:将模型名的点号替换为下划线,加前缀model_。如为library.book模型生成的外部标识符,由library_app模块创建,为library_app.model_library_book。定义权限ACL的CSV文件通常需要这些XML ID。
读者已熟悉了定义模型的那些选项。下一步就来了解几种字段类型以及它们的配置选项。
创建字段
创建新模型后的第一步是添加字段。Odoo 支持你能想到的所有基本数据类型,如文本字符串、整型、浮点型、布尔型、日期、时间以及图片或二进制数据。
下面就来看看 Odoo 中的一些字段类型吧。
基本字段类型
我们将为图书模型添加几种可用的字段类型。
编辑library_app/models/library_book.py文件的Book类,将当前的字段定义换成以下这样:
class Book(models.Model):
_name = "library.book"
_description = "Book"
# String fields
name = fields.Char('Title', required=True)
isbn = fields.Char('ISBN')
book_type = fields.Selection(
[('paper', 'Paperback'),
('hard', 'Hardcover'),
('electronic', 'Electronic'),
('other', 'Other')],
'Type')
notes = fields.Text('Internal Notes')
desc = fields.Html('Description')
# Numeric fields:
copies = fields.Integer(default=1)
avg_rating = fields.Float('Average Rating', (3,2))
price = fields.Monetary('Price', 'currency_id')
currency_id = fields.Many2one('res.currency') # price helper
# Date and time fields
date_published = fields.Date()
last_borrow_date = fields.Datetime(
'Last Borrowed On',
default=lambda self: fields.Datetime.now())
# Other fields
active = fields.Boolean('Active?', default=True)
image = fields.Binary('Cover')
# Relational Fields
publisher_id = fields.Many2one('res.partner', string='Publisher')
author_ids = fields.Many2many('res.partner', string='Authors')
这些Odoo中的非关联字段类型的示例,每个还可以接收一些位置参数。接下来我们就讲解所有这些类型及其选项。
小贴士:
Python 中有两类参数:位置参数和关键字参数。
位置参数需按指定顺序使用。例如,f(x, y) 应以f(1, 2)方式调用。
关键字参数通过参数名传递。如同一个例子,可使用f(x=1, y=2)甚至是f(1, y=2)两种传参方式混用。
但请注意位置参数必须要放在关键字参数之前,f(x=1, 2)是不允许的。更多有关关键字参数知识参见 Python 官方文档。
一般来说,第一个位置参数是字段标题,与string关键字参数相对应。例外情况是Selection字段及所有关联字段。
string标签用于用户界面标签的默认文本。如未传string属性,会根据字段名将下划线替换为空格并将单词首字母大写来自动生成。例如,date_published的默认标签为Date Published。
作为参考,以下是所有非关联字段类型及它们的位置参数:
- Char(string)是简单文本字段,唯一位置参数是字段标签。
- Text(string)是多行文本字段,唯一位置参数是string字段标签。
- Selection(selection, string)是下拉选择列表。选项位置参数是一个 [("value", "Description"),] 元组列表。元组第一个元素是存储在数据库中的值,第二个元素是展示在用户界面中的描述。该列表可由其它模块使用selection_add关键字参数扩展。
- Html(string)存储为文本字段,但有针对用户界面 HTML 内容展示的特殊处理。出于安全考虑,该字段会被清洗,但清洗行为可使用sanitize=False重载。
- Integer(string)为整型数字,仅需字段标签字符串参数。
- Float(string, digits)存储浮点数字,第二个可选参数为使用的精度。这是一个(n, d) 元组,n为总位数,d为小数位数。
- Monetary(string, currency_field)与浮点字段类似,但包含货币值的特殊处理。第二个参数currency_field为货币字段的名称。默认设置为currency_field="currency_id"。
- Date(string)和Datetime(string)为日期和日期时间值。只需一个标签文本作为位置参数。
- Boolean(string)存储True 或False值。只需一个标签文本作为位置参数。
- Binary(string)存储包括图片在内的二进制数据。只需一个字符串标签位置参数。
这些字段定义包夜了常用的基本参数。注意这些并不是必填,在没填写时Odoo会补充合适的默认值。
Odoo 12中的变化
Date和Datetime字段在 ORM 中以Python日期对象进行处理。此前的版本中按文本字符串处理。因此在进行操作时需显式转化为Python 日期对象,之后还要再转化为文本字符串。
文本字符串,包括Char、Text和Html有一些特有属性:
- size (仅用于Char字段)设置最大允许尺寸。无特殊原因建议不要使用,例如可用于具有最大允许长度的社保账号。
- translate=True使得字段内容可翻译,带有针对不同语言的不同值。
- trim默认值为 True,在网页客户端中自动去除周围的空格。可通过设置trim=false来取消。
Odoo 12中的变化
trim字段属性在 Odoo 12中引入。此前版本中文本字段保存前后的空格。
除这些以外,还有在后面会介绍到的关联字段。这在本单后面的模型间的关联一节中进行讲解。
不过, 我们还要先在下一小节中了解下有关基础字段类型属性的其它知识。
常用字段属性
至此,我们学习了几种基本字段类型的基础位置参数。但还有一些其它属性。
以下是所有字段类型中可以使用的关键字参数:
- string是字段的默认标签,在用户界面中使用。除Selection和关联字段外,它都是第一个位置参数,所以大多数情况都以关键字参数使用。如未传入,将由字段名自动生成。
- default设置字段默认值。可以是具体值(如 active字段中的default=True),或是可调用指针,有名函数指针或lambda匿名函数均可。
- help提供 UI 中鼠标悬停字段向用户显示的提示文本。
- readonly=True会使用户界面中的字段默认不可编辑。在 API 层面并没有强制,模型方法中的代码仍然可以向其写入,视图定义要可进行重载。仅针对用户界面设置。
- required=True使得在用户界面中字段默认必填。这通过在数据库层面为字段添加NOT NULL 约束来实现。
- index=True为字段添加数据库索引,让搜索更快速,但同时也会部分占用更大的磁盘空间、降低写操作速度。
- copy=False让字段在使用 ORM copy()方法复制记录时忽略该字段。除对多关联字段默认不复制,其它字段值默认会被复制。
- deprecated=True将字段标记为弃用。字段仍可正常使用,但每次访问会在服务端日志中写入警告消息。
- groups可限制字段仅对一些组可访问、可见。值为逗号分隔的权限组XML ID列表,如groups="base.group_user,base.group_system"。
- states传入依赖 state字段值的 UI 属性字典映射值。可用属性有readonly、required和invisible,例如states={'done':[('readonly',True)]}。
小贴士:注意states 字段等价于视图中的 attrs 属性。同时注意视图也支持 states 属性,但用途不同,传入逗号分隔的状态列表来控制元素什么时候可见。
以下为使用了所有字段属性关键字参数的name字段示例:
name = fields.Char(
"Title",
default=None,
help="Book cover title.",
readonly=False,
required=True,
index=True,
copy=False,
deprecated=True,
groups="",
states={},
)
此前的Odoo版本支持oldname="field"属性,用于在新版本中重命名字段。将旧字段的数据在模块升级过程中自动拷贝到新字段。
Odoo 13中的变化
移除了oldname字段属性,不再可用。替代方式是使用迁移脚本。
以上是通用的字段属性,可用于所有字段类型。接下来我们要学习对字段设置默认值。
设置默认值
如前所述,default 属性可带有固定值,或引用函数来动态计算默认值。
对于简单运算,可使用 lambda 函数来避免过重的有名函数或方法的创建。以下是一个计算当前日期和时间默认值的常用示例:
last_borrow_date = fields.Datetime(
'Last Borrowed On',
default=lambda self: fields.Datetime.now(),
)
default的值也可以是一个函数引用。可以是名称引用或函数名字符串。
下例中使用了对_default_last_borrow_date函数方法的名称引用:
或待定义函数名字符串:
def _default_last_borrow_date(self):
return fields.Datetime.now()
last_borrow_date = fields.Datetime(
'Last Borrowed On',
default=_default_last_borrow_date,
)
下例的作用相同,但使用函数名称字符串:
last_borrow_date = fields.Datetime(
'Last Borrowed On',
default="_default_last_borrow_date",
)
def _default_last_borrow_date(self):
return fields.Datetime.now()
后一种方法,函数名的解析延迟至运行时,而不是在Python文件加载的时候。因此,在第二个示例中,我们可以引用在代码中靠后声明,而第一个示例中,必须在使用前声明函数。
然而普遍的代码规范是在字段定义之前定义默认值函数。另外使用第一个例子中的函数名引用有一个好处,如果支持静态代码分析的话代码编辑器可以监测到类型错误。
自动字段名
一些字段名很特别,可能是因为它们出于特殊目的作为 ORM 保留字,或者是由于内置功能使用了一些默认字段名。
id 字段保留用作标识每条记录的自增数字以及数据库主键,每个模型都会自动添加。
以下字段只要模型中没设置_log_access=False都会在新模型中自动创建:
- create_uid为创建记录的用户
- create_date是记录创建的日期和时间
- write_uid是最后修改记录的用户
- write_date是最后修改记录的日期和时间
每条记录的这些字段信息都可通过开发者菜单下的View Metadata进行查看。
上述的这些字段名在Odoo框架中有特殊含义。除它们之外,还有一些字段名用作Odoo某些功能的默认字段。在下一节中进行讲解。
保留字段名
一些内置 API 功能默认需要一些指定字段名。这些是保留字段名,应避免误用。
这些保留字段如下:
- Char类型的name或x_name:默认用作记录的显示名称。其它字段也可使用_rec_name模型属性设置显示名称。非字符字段类型也可使用它,会强制将数据转化为文本。
- 布尔类型的active和x_active: 允许我们关闭记录,让记录隐藏。带有active=False的记录会自动从查询中排除掉,除非是在环境上下文中添加{'active_test': False} 。它可用于记录存档或假删除(soft delete)。
- Selection类型的state: 表示记录生命周期的基本状态。它允许使用states字段属性动态设置为readonly、required或invisible,例如states={'draft': [('readonly', False)]}。
- 多对一字段的parent_id:用于定义树状层级结构,在域表达式中启用child_of和parent_of运算符。用作parent_id的字段可使用_parent_name模型属性设置为其它字段。
- Char类型的parent_path:用于在域表达式中优化child_of和parent_of运算符的使用。为正常操作,用add index=True来使用数据库索引。我们会在本章稍后的层级关联一节中进行讨论层级关联。
- Many2one类型的company_id:用于标识记录所属的公司。留空表示记录在各个公司中共享。通过_check_company函数对公司数据一致性进行内部检查。
Odoo 14中的变化
x_active现在进行了重组,和active字段的效果一致。引用它是为了更好的支持开发者模式或Odoo Studio应用的自定义。
到目前为止我们讨论的都是非关联字段。但应用数据结构中很大一部分是描述实体间关联的。下面就一起来学习。
模型间的关联
中大型业务应用需要在涉及到的不同实体间的使用关联。要予以实现,需要使用关联字段。
再来看看我们的图书应用,图书模型中有如下关联:
- 每本书有一个出版社,每个出版社有多本书。从书的视角看,这是一种多对一关联。通过有数据库的一个整型字段实现,存储关联出版社记录的 ID,并放一个数据库外键来强化引用的正确性。
- 反过来,从出版社的角度看,是一对多关联,即出版社可以出版多本书。虽然这也是一种Odoo字段类型,但其数据库层面依赖于多对一关联。我们通过对图书查询知道了关联的出版社,过滤出出版社ID。
- 每本书可以有多名作者,每个作者可以写多本书。这是多对多关联。反过来还是多对多关联。在关联的数据库中,多对多关联通过一个辅助数据表来实现。Odoo会自动进行处理,当然我们希望的话也可以通过技术进行掌控。
下面的小节中我们就会分别讨论这些关联。
具体的用例就是层级关联,即一个模型中的记录与同模型中的其它记录关联。我们将引入一个图书分类模型讲解这一情况。
最后,Odoo 框架还支持弹性关联,即一个字段可与多个模型建立关联,这称为引用字段。
多对一关联
多对一关联是对其它模型中记录的引用。例如在图书模型中,publisher_id表示图书出版社,是对partner记录的一个引用。
以防忘记,如下是使用纯位置参数定义的出版社字段:
publisher_id = fields.Many2one(
'res.partner', 'Publisher')
以上多对一字段定义使用了位置参数:
- 第一个位置参数是关联模型,对应comodel关键字参数,本例中的值为res.partner。
- 第二个位置参数是字段标签,对应string关键字参数。但它和其它关联字段不同,所以推荐一直使用string关键字参数。
多对一模型字段在数据表中创建一个字段,并带有指向关联表的外键,其中为关联记录的数据库 ID。
关键字参数用用于代替或补充位置参数。以下是多对一字段所支持的的关键字参数:
- ondelete定义关联记录删除时执行的操作,可使用如下行为
- set null (默认值): 关联字段删除时会置为空值
- restricted:抛出错误阻止删除
- cascade:在关联记录删除时同时删除当前记录
- context是一个数据字典,可在浏览关联时为网页客户端传递信息,比如设置默认值。第八章 Odoo 15开发之业务逻辑 - 业务流程的支持中会做深入讲解。
- domain是一个域表达式:使用一个元组列表过滤记录来作为关联记录的选项,第八章 Odoo 15开发之业务逻辑 - 业务流程的支持中会详细讲解。
- auto_join=True在使用该关联进行搜索时允许ORM使用SQL连接。使用时会跳过访问权限规则,用户可以访问权限规则不允许其访问的关联记录,但这样 SQL 的查询会更快。
- delegate=True 创建关联记录的代理继承。使用时还必须设置required=True和ondelete='cascade'。代理继承更多知识参见第四章 Odoo 15开发之模块继承。
一对多反向关联
一对多关联是多对一关联的反向关联。它列出引用该记录的关联模型记录。
比如在图书模型中,publisher_id字段与 parnter 模型是一个多对一关联。这说明partner与图书模型可以有一个一对多的反向关联,列出每个出版商出版的图书。
在创建一对多关联前,应在其关联模型中创建一个多对一关联。为此,在library_app/models/res_partner.py文件中添加如下代码:
from odoo import fields, models
class Partner(models.Model):
_inherit = 'res.partner'
published_book_ids = fields.One2many(
'library.book', # 关联模型
'publisher_id', # fields for "this" on related model
string='Published Books')
我们向模块添加了新文件,所以不要忘记在library_app/models/__init__.py中导入该文件:
from . import library_book
from . import res_partner
One2many字段接收三个位置参数:
- 关联模型 ,对应comodel_name关键字参数
- 引用该记录的模型字段 ,对应inverse_name关键字参数
- 字段标签 ,对应string关键字参数
其它可用的关键字参数与多对一字段相同:context、domain、auto_join和ondelete(此处作用于关联中的 many 这一方)。
多对多关联
在两端都存在对多关联时使用多对多关联。还是以我们的图书应用为例,书和作者之间是多对多关联:一本书可以有多个作者,一个作者可以有多本书。
图书端有的library.book模型,有如下定义:
class Book(models.Model):
_name = 'library.book'
author_ids = fields.Many2many(
'res.partner',
string='Authors')
在作者端,我们也可以为res.partner添加一个反向关联:
class Partner(models.Model):
_inherit = 'res.partner'
book_ids = fields.Many2many(
'library.book',
string='Authored Books')
Many2many最小化签名要包含一个关联模型位置参数(comodel_name关键字参数),推荐使用string参数添加字段标签。
在数据库层面上,多对多关联不会在已有表中添加任何字段。而是自动创建一个关联表来存储记录间的关联,这张特殊的表仅有两个 ID 字段,为两张关联表的外键。
默认关联表名由两个表名中间加下划线并在最后加上_rel 来组成。本例的图书和作者关联,表名应为library_book_res_partner_rel。
有时我们可能需要重写这种自动生成的默认值。一种情况是关联模型名称过长,导致自动生成的关联表名的长度超出PostgreSQL数据库63个字符的上限。这时就需要手动选择一个关联表名来符合字符数的要求。
另一种情况是我们需要在相同模型间建立第二个多对多关联。这时也需要手动提供一个关联表名来避免与已存在的第一张表名冲突。
有两种方案来重写关联表名:位置参数或关键字参数。
通过字段定义的位置参数的示例如下:
# Book <-> Authors关联(使用位置参数)
author_ids = fields.Many2many(
'res.partner', # 关联模型(尾款)
'library_book_res_partner_rel', # 要使用的关联表名
'a_id', # 本记录关联表字段
'p_id', # 关联记录关联表字段
'Authors') # string标签文本
要使可读性更强,也可使用关键字参数:
# Book <-> Authors关联(使用关键字参数)
author_ids = fields.Many2many(
comodel_name='res.partner', # 关联模型(必填)
relation='library_book_res_partner_rel', # 关联表名
column1='a_id', # 本记录关联表字段
column2='p_id', # 关联记录关联表字段
string='Authors') # string标签文本
以下为此处使用的参数:
- comodel_name 是关联模型的名称。
- relation 是支撑关联数据的数据表名。
- column1 是指向模型记录的字段名。
- column2 是指向关联模型记录的字段名。
- string 是用户界面中的字段标签。
与一对多关联字段相似,多对对字段还可以使用context、domain和auto_join这些关键字参数。
小贴士:在抽象模型中,多对多字段不要使用column1和column2属性。在 ORM 设计中对抽象模型有一个限制,如果指定关联表列名,就无法再被正常继承。
父子关联是值得深入研究的特定情况。下一节中我们进行学习。
层级关联
父子树状关联使用同一模型中多对一关联表示,其中每条记录指向其父级。反向的一对多关联表示记录的直接子级。
Odoo 通过域表达式所带的child_of和parent_of运算符改良了对这些层级数据结构的支持。只要这些模型有parent_id字段(或_parent_name有效模型定义,设置替代的字段名)就可以使用这些运算符。
通过设置_parent_store=True模型属性和添加parent_path辅助字段可加快等级树的查询速度。该辅助字段存储用于加速查询速度的层级树结构信息。
Odoo 12中的变化
parent_path辅助字段在 Odoo 12中引入。此前版本中使用parent_left和parent_right整型字段来实现相同功能,但在 Odoo 12中弃用了这些字段。
为演示层级结构,我们将为图书应用添加一个分类树,用于为图书分类。
在library_app/models/library_book_category.py文件中添加如下代码:
from odoo import api, fields, models
class BookCategory(models.Model):
_name = 'library.book.category'
_description = 'Book Category'
_parent_store = True
name = fields.Char(translate=True, required=True)
# Hierarchy fields
parent_id = fields.Many2one(
'library.book.category',
'Parent Category',
ondelete='restrict')
parent_path = fields.Char(index=True)
# Optional but good to have:
child_ids = fields.One2many(
'library.book.category',
'parent_id',
'Subcategories')
这里定义了一个基本模型,包含指向父级记录的parent_id字段。
为更快的树级搜索,添加了一个_parent_store=True 模型属性。使用该属性必须还要添加parent_path字段,并为其建索引。指向父级的字段名应为parent_id,但如果声明了可选的_parent_name模型属性,则可以使用其它字段名。
添加字段列出直接的子非常方便。即为上述代码中的一对多反向关联。
还有不要忘记在library_app/models/__init__.py
文件中添加对以上代码的引用:
from . import library_book
from . import res_partner
from . import library_book_category
注意这些增加的运算占用存储并且存在执行时间的消耗,所以适于读多写少的场景使用,如本例中的分类树。仅在优化多节点的深度层级时有必要使用,在小层级或浅等级场景可能会被误用。
使用引用字段的弹性关联
普通关联字段只能指向一个固定的关联模型,但Reference字段类型不受这一限制,它支持弹性关联,因此同一字段可指向多个目标模型。
作为示例,我们为图书分类模型来添加引用字段,来表示重点图书或作者。因此该字段可引用图书或 partne记录:
highlighted_id = fields.Reference(
[('library.book', 'Book'), ('res.partner', 'Author')],
'Category Highlight'
)
该字段定义与 Selection 字段相似,但这里选择列表的内容为用于该字段的模型。在用户界面中,用户会先选择列表中的模型,然后选择模型中的指定记录。
引用字段可以字符字段存储在数据库中,包含<model>、<id> 字符串。
Odoo 12中的变化
在此前Odoo版本中通过菜单Settings > Technical > Database Structure配置在引用字段中可用的模型。这些配置可通过在模型选择列表中添加odoo.addons.res.res_request.referenceable_models函数,在引用字段在中使用这些配置。Odoo 12中删除了这一配置。
这样我们就学习了Odoo所支持的字段类型。字段不仅能存储用户添加的数据,还能展示计算值。在下一节中讨论这一功能。
计算字段
字段值除普通的读取数据库中存储值外,还可自动由函数计算。计算字段的声明和普通字段相似,但有一个额外的compute参数来定义用于计算的函数。
计算字段包含部分业务逻辑的编写。因此要完全使用这一功能,还应熟悉第八章 Odoo 15开发之业务逻辑 - 业务流程的支持中讲解的知识。此处我们将解释计算字段用法,但业务逻辑会尽量保持简单。
我们的例子为Books模型添加计算字段,显示出版商的国家。这样会在表单视图中显示国家。
找到该值的代码很简单:如果book表示图书记录。可使用点号标记符book.publisher_id.country_id获取出版商的国家。
编辑library_app/models/library_book.py文件中的图书模型,代码如下:
publisher_country_id = fields.Many2one(
'res.country', string='Publisher Country',
compute='_compute_publisher_country'
)
@api.depends('publisher_id.country_id')
def _compute_publisher_country(self):
for book in self:
book.publisher_country_id = book.publisher_id.country_id
首先,以上代码添加了一个publisher_country_id字段,和一个计算属性,包含计算其值的_compute_publisher_country方法。
方法以字符串参数传入字段中,但也可以传递一个可调用引用(方法标识符,不带引号)。但这时需确保Python 文件中方法在字段之前定义。
计算方法名的代码规范是在计算字段名前加上_compute_前缀。
_compute_publisher_country方法接收self记录集来进行运算,应当设置所有这些记录的计算字段值。要对self记录集进行遍历来设置所有记录。
计算的值通过常规的赋值(写入)操作进行设置。本例中的计算相当简单:使用当前图书的publisher_id.country_id值对其赋值。
小贴士:同一计算方法可用地对两个或多个字段进行计算。此时,方法应用于所计算字段的compute属性中,计算方法应为所有这些字段赋值。
计算方法必须要对一个或多个字段赋值。如果计算方法有 if 条件分支,确保每个分支中为计算字段赋了值。否则在未赋值的分支中将会报错。
ODOO 13中的变化
Odoo 13引入了计算可写字段,准备在未来替换掉onchange机制。计算可写字段有一个计算逻辑,在依赖发生变化时触发,还允许用户直接设置其值。这一机制会在第八章 Odoo 15开发之业务逻辑 - 业务流程的支持中和onchange一起讲解。
需要@api.depends装饰器指定计算所依赖的字段。ORM使用它来知晓何时触发计算更新存储值或缓存值。可接受一个或多个字段名参数并可使用点号标记法来跟踪字段关联。本例中,在publisher_id.country_id发生变化时应当重新计算publisher_country_id字段。
警告:
忘记对计算字段添加 @api.depends装饰器或是没有添加计算使用的所有依赖字段,计算字段就不会按计划重新计算。这种bug很难发现。
可通过在图书表单视图中添加publisher_country_id字段来查看效果,位于library_app/views/library_book.xml文件中。在网页中查看时要确保所查看的记录已选择了有国家的出版商。
搜索和写入计算字段
我们刚刚创建的计算字段可读取但不可搜索或写入。默认情况下计算字段是实时计算的,值不存储在数据库中。这也是无法像普通字段那样进行搜索的原因。
突破这种限制的一种方式是通过添加store = True属性让计算的值存储在数据库中。在任一依赖发生变化时就会重新计算。因为值进行了存储,所以就可以像普通字段那样进行搜索,无需搜索函数。
计算字段还支持不进行存储的搜索及写入操作。可通过和计算方法一并实现特殊的方法来达成:
- search方法来实现搜索逻辑
- inverse方法来实现写入逻辑
使用这些方法,可修改计算字段的声明如下:
publisher_country_id = fields.Many2one(
'res.country', string='Publisher Country',
compute='_compute_publisher_country',
inverse='_inverse_publisher_country',
search='_search_publisher_country',
)
计算字段中的写入是计算的反向(inverse)逻辑。因此处理写入操作的方法称为 inverse。
本例中设置publisher_country_id的值会修改出版商的国家。
注意这也会改变同一出版商所有图书中看到的值。常规的权限控制同样适用于这些写操作,因此仅在当前用户对partner模型也具有写权限时才会成功。
inverse方法的实现使用值对计算字段进行设置,执行需让修改持久化所需的实际写操作:
def _inverse_publisher_country(self):
for book in self:
book.publisher_id.country_id = book.publisher_country_id
原始的值运算将book.publisher_id.country_id的值拷贝到book.publisher_country_id字段中。逆向运算如以上代码所示,进行了反向操作。读取book.publisher_country_id 所设置的值并写入到book.publisher_id.country_id字段中。
要为计算字段开启搜索操作,需要实现search 方法。search方法拦截计算字段上运行的域表达式,然后用仅使用普通存储字段的替代域表达式进行替换,
在publisher_country_id的示例中,实际的搜索是对关联的publisher_id相应记录的country_id字段进行操作。翻译为方法实现如下:
def _search_publisher_country(self, opearator, value):
return [('publisher_id.country_id', operator, value)]
在模型上执行搜索时,域表达式元组是参数,提供运算符的详情以及域表达式中使用的值。
search方法在域表达式的条件中存在该计算字段时触发。接收搜索所需要的运算符和值,然后转译为原始搜索元素再加到替代的搜索或表达式中。country_id字段存储在关联的partner模型中,因此我们的搜索实现仅需修改原搜索表达式为使用publisher_id.country_id字段。
有关域表达式在第八章 Odoo 15开发之业务逻辑 - 业务流程的支持会做详细讲解。
关联字段
前面一节我们实现的计算字段仅仅是从关联记录中将值拷贝到模型自己的字段中。通常用于在表单中显示关联记录的字段。Odoo框架为此提供了一种简写:关联字段(related field)功能。
关联字段令关联模型中的字段在当前模型中可用,通过点号标记调用链访问。这让那些点号标记符本无法使用之处可以访问关联字段,如UI表单视图。
要创建关联字段,需要声明所需类型的字段,还要使用related属性,通过点号标记字段链来访问目标关联字段。
关联字段可用于达到与前述publisher_country_id计算字段相同的效果。
使用关联字段的实现如下:
publisher_country_id = fields.Many2one(
'res.country', string='Publisher Country',
related='publisher_id.country_id',
)
本质上关联字段仅仅是快捷实现 search 和 inverse 方法的计算字段。也就是说可以直接对其进行搜索和写入。
默认关联字段是只读的,因而反向的写操作不可用。可通过设置readonly=False字段属性来开启写操作。
Odoo 12中的变化
此前的Odoo版本中,关联字段默认可写,但实践证明这个默认值很危险,因为它可能在未允许的情况下修改配置或主数据。因此,从Odoo 12开始,关联字段默认为只读:readonly=True。
值得一提的是关联字段也可以像其它计算字段一样使用store=True将值存储在数据库中。
这样我们就学习完了Odoo字段所支持的功能,包括计算字段。另一个有关数据结构的重要元素是约束,强化数据质量和完备性。我们在下一节中讨论。
模型约束
通常应用需保证数据完备性,并执行一些验证来保证数据是完整和正确的。
PostgreSQL数据库管理器支持很多有用的验证:如避免重复,或检查值以符合某些简单条件。Odoo模型可以利用 PostgreSQL约束功能。
一些检查要求更复杂的逻辑,最好是使用 Python 代码来实现。对于这些情况,我们可使用特定的模型方法来实现 Python 约束逻辑。
我们一起来学习这两种方式。
SQL模型约束
SQL约束加在数据表定义中,并由PostgreSQL直接执行。借由_sql_constraints类属性来定义。
这是一个元组组成的列表,并且每个元组的格式为(name, sql, message):
- name是约束标识名
- sql是约束的PostgreSQL语法
- message是在约束验证未通过时向用户显示的错误消息
最常用的SQL约束,用于防止重复数据,还有CHECK约束,用于使用SQL表达式对数据进行测试。
举个例子,我们为Book模型添加两个约束:
- 确保没有带相同标题和出版日期的重复书籍。
- 确保出版日期不是未来的日期。
编辑library_app/models/library_book.py文件添加如下代码实现这两个约束。通常会把它放在字段声明的代码之后:
_sql_constraints = [
('library_book_name_date_uq', # 约束唯一标识符
'UNIQUE (name, date_published)', # 约束 SQL 语法
'Book title and publication date must be unique'), # 消息
('library_book_check_date',
'CHECK (date_published <= current_date)',
'Publication date must not be in the future.'),
]
更多有关PostgreSQL约束语法,请参见官方文档。
Python模型约束
Python 约束可使用自定义代码来执行验证。验证方法应添加@api.constrains装饰器,并且包含要检查的字段列表。其中任意字段被修改就会触发验证,并且在未满足条件时抛出异常,通常报的是ValidationError。
就图书应用来说,一个明显的示例就是防止插入不正确的 ISBN 号。我们已经在_check_isbn()方法中编写了 ISBN 的校验逻辑。可以在模型约束中使用它来防止保存错误数据。
编辑library_app/models/library_book.py文件,在文件顶部添加导入语句:
from odoo.exceptions import ValidationError
接下来在同一个文件中为Book类添加如下代码
@api.constrains('isbn')
def _constrain_isbn_valid(self):
for book in self:
if book.isbn and not book._check_isbn():
raise ValidationError('%s is an invalid ISBN' % book.isbn)
Python约束通常放在字段声明的代码之前。
Odoo的 base 模型总览
在前面文章中,我们一起新建了模型,如图书模型,但也使用了已有的模型,如 Odoo base模块自带的Partner 模型。下面就来介绍下这些内置模型。
Odoo框架内核中有一个base插件模块。它提供了 Odoo 应用所需的基本功能。位于Odoo代码库的./odoo/addons/base子目录中。
提供了Odoo中的官方应用和功能的标准插件模块,依赖并基于base模块构建。标准插件位于Odoo代码库的./addons子目录中。
base模块中包含两类模型:
- 信息仓库(Information Repository), ir.*模型
- 资源(Resources), res.*模型
信息仓库用于存储 Odoo 框架所需的基础数据,如菜单、视图、模型、动作等。Technical菜单下的数据通常都存储在信息仓库模型中。
相关的例子有:
- ir.actions.act_window用于窗口动作
- ir.config_parameter用于全局配置项
- ir.ui.menu用于菜单项
- ir.ui.view用于视图
- ir.model用于模型
- ir.model.fields用于模型字段
- ir.model.data用于XML ID
资源模型存储所有模块所使用的基础主数据。
以下是一些重要的资源模型:
- res.partner用于业务伙伴,如客户、供应商和地址
- res.company用于公司数据
- res.country用于国家
- res.country.state用于国家内的州或区域
- res.currency用于货币
- res.groups用于应用权限组
- res.users用于应用用户
这些应该有助于你在未来遇到这些模型时理解它们来自何处。
总结
学习完本章,我们熟悉了各种模型类型,如临时模型和抽象模型,以及它们对于用户界面向导和mixin中间件的用处。其它的模型功能有Python和SQL约束,可用于防止数据词条错误。
我们还学习了可用的字段类型,以及它们所支持的所有属性,这样可以尽可能精确的方式表示业务数据。我们也学习了关联关系字段,以及如何使用它们创建应用所需的不同实体间的关联关系。
然后,我们了解到模型通常继承models.Model类,但也可以使用models.Abstract来创建可复用的mixin,使用models.Transient来创建向导或高级用户交互对话框。我们学习了常用的模型属性,如_order用于排序,_rec_name指定记录展示时默认使用的字段。
模型中的字段定义了所有要存储的数据。同时了解了非关联字段类型及它们支持的属性。我们也学习了一些关联字段类型:多对一、一对多和多对多,以及如何在模型间定义关联,包括层级父子关联。
大部分字段在数据库中存储用户的输入,但字段也可以通过 Python 代码自动计算值。我们学习了如何实现计算字段,以及一些高级用法,如使计算字段可写及可搜索。
模型定义还有一部分是约束,强化数据一致性和执行验证。可以通过PostgreSQL或Python代码实现。
一旦我们创建了数据模型,就应该为它提供一些默认和演示数据。在下一章中我们将学习如何使用数据文件在系统中导入、导出和加载数据。
扩展阅读
有关模型的官方文档。