odoo-ORM1

业务逻辑层和数据库层的桥梁

本文参考: ORM API — Odoo 18.0 documentation

1、ORM介绍


ORM概念

对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。

简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系数据库中。

ORM在业务逻辑层和数据库层之间充当了桥梁的作用。几乎所有的软件开发过程中都会涉及到对象和关系数据库。

在用户层面和业务逻辑层面,我们是面向对象的。当对象的信息发生变化的时候,我们就需要把对象的信息保存在关系数据库中。

按照传统的方式来进行开发就会出现程序员会在自己的业务逻辑代码中夹杂很多SQL语句用来增加、读取、修改、删除相关数据,而这些代码通常都是重复的。

ORM的优势

1、ORM解决的主要问题是对象和关系的映射。它通常把一个类和一个表一一对应,类的每个实例对应表中的一条记录,类的每个属性对应表中的每个字段。 

2、ORM提供了对数据库的映射,不用直接编写SQL代码,只需像操作对象一样从数据库操作数据。

3、让软件开发人员专注于业务逻辑的处理,提高了开发效率。

ORM的劣势

1、ORM的缺点是会在一定程度上牺牲程序的执行效率。

2、对开发人员来说,ORM用久了可能SQL语句就不会写了,关系数据库相关技能退化。


2、模型

模型字段定义为模型本身的属性:

from odoo import models, fields
class AModel(models.Model):
    _name = 'a.model.name'

    field1 = fields.Char()

代码解释:

1、模型的定义

class AModel(models.Model):

这是在定义一个模型类,继承自 Odoo 的 models.Model,表示这是一个 Odoo 模型。

2. 模型名称

_name = 'a.model.name'

_name 是一个特殊属性,用于指定模型的唯一标识符。这个名称通常决定了数据库表的名称。

这意味着您不能使用相同的_name 时,最后一个将默认​覆盖前一个。​

3. 字段的定义

field1 = fields.Char()

这是模型中的一个字段。字段是模型的属性,它描述了模型数据的类型和行为:

  • 字段名:field1 是字段的标识符。
  • 字段类型:fields.Char() 定义了一个字符字段(通常对应数据库中的 VARCHAR 类型)。
  • 字段用途:这个字段用来存储模型的一个字符类型数据,例如名字、描述等。
  • 参数: 
    • 默认情况下,字段的标签(用户可见名称)是 字段变量名,可以使用 string参数 覆盖。field2 = fields.Integer(string="Field Label")。
    • 可以使用参数给字段设置默认值。name = fields.Char(default="a value")
    • 也可以调用函数计算复杂的默认值,该函数应返回该字段对应的值:
def _default_name(self):
    return self.get_value()

name = fields.Char(default=lambda self: self._default_name())

4. 数据库结构和模型的映射

当我们定义了字段,Odoo 会自动将它映射到数据库中的表和列。通过这些字段,开发者可以直接在代码中访问和操作存储在数据库中的数据,而不需要手动编写 SQL 语句。

3、odoo三种模型

Odoo 模型通过继承以下类之一创建:

  1. Model: 常规模型,对应实际的数据库表,适合大多数场景。
  2. TransientModel:用于临时数据,这些数据存储在数据库中,但会定期自动清理。 例如向导(Wizard)的数据,虽然存储在数据库中,但 Odoo 会定期清理其数据。
  3. AbstractModel:用于抽象父类,供多个继承模型共享使用。 用作通用逻辑的基类,不能单独使用,且默认不会创建数据库表。

系统会为每个数据库自动实例化每个模型一次。这些实例表示每个数据库中可用的模型,具体取决于安装在该数据库中的模块。每个实例的实际类由创建和继承对应模型的 Python 类构建。

每个模型实例是一个“记录集”(recordset),即该模型记录的有序集合。方法(如 browse()、search())或字段访问会返回记录集。

记录本身没有显式表示:记录以包含单个记录的记录集形式表示。

要创建一个不应被实例化的类,可以将 _register 属性设置为 False。

其他的属性

_auto= False

决定是否为模型创建数据库表。如果设置为 False,需要通过覆盖 init() 方法手动创建数据库表。

  • 默认值:
    • 对于 Model 和 TransientModel,默认为 True(自动创建表)。
    • 对于 AbstractModel,默认为 False(不会创建表)。

提示

如果需要创建不包含数据库表的模型,请继承 AbstractModel。

_log_access

决定是否由 ORM 自动生成和更新访问日志字段。

  • 默认值与 _auto 的值相同。

_table= None

模型对应的 SQL 表名,当 _auto 为 True 时生效。

_sql_constraints

格式为 [(name, sql_def, message)] 的 SQL 约束列表,用于定义数据库层面的约束条件。

_register= False

决定模型在 Odoo 注册表中的可见性。

_abstract= True

是否为抽象模型。

参考

抽象模型 AbstractModel。

_transient= False

是否为临时模型。

参考

临时模型 TransientModel。

_name: str | None = None

模型的内部名称(使用点号表示模块命名空间,例如 module_name.model_name)。

_description: str | None = None

模型的描述性名称,用于表示模型的非正式名称。

_inherit

Python 的继承模型:

  • 类型:str 或 list[str] 或 tuple[str, ...]。
  • 如果设置了 _name,表示继承父模型的所有字段,并在数据库中创建了新的表。
  • 如果未设置 _name,表示对一个现有模型进行扩展。

_inherits= {}

字典,格式为 {‘parent_model’: ‘m2o_field’},表示业务对象的组合式继承。

  • 新模型会暴露继承模型的所有字段,但这些字段不存储在新模型中,其值存储在链接的记录中。
  • 示例:
_inherits = {
    'a.model': 'a_field_id',
    'b.model': 'b_field_id'
}

警告

如果多个继承的模型中定义了同名字段,则最终继承的字段为 _inherits 字典中顺序靠后的模型字段。

_rec_name= None

用于标记记录的字段名,默认值为 name。

_order= 'id'

搜索结果的默认排序字段。

_check_company_auto= False

在写入或创建记录时,检查关联字段的公司一致性(通过 _check_company=True 属性)。

_parent_name= 'parent_id'

定义用于表示父字段的 many2one 字段。

_parent_store= False

如果设置为 True,则会计算 parent_path 字段,并建立一个索引来存储树形结构。

  • 配合 parent_path 字段,支持通过 child_of 和 parent_of 域运算符进行更快的层级查询。

_fold_name= 'fold'

用于在看板(Kanban)视图中决定是否折叠分组的字段。

1、odoo.models.BaseModel 

odoo.models.BaseModel 是 Odoo 模型的基础类。 odoo.models.AbstractModel 是 odoo.models.BaseModel 的别名。


2、odoo.models.Model


odoo.models.Model 是 Odoo 常规数据库持久化模型的主类。

Odoo 模型通过继承此类创建,例如:

class user(model.Model):
    ...

系统会在每个安装了该类模块的数据库中实例化此类一次。

3、odoo.models.TransientModel

odoo.models.TransientModel 是临时记录模型的父类,其记录用于临时存储,且会定期自动清理。

  • 访问权限
    • 所有用户可以创建新的记录。
    • 用户只能访问自己创建的记录。
    • 超级用户对所有 TransientModel 记录具有不受限制的访问权限。
  • _transient_max_count= 0
    最大临时记录数,默认为 0(无限制)。
  • _transient_max_hours= 1.0
    最大空闲存活时间(单位:小时),默认为 1.0 小时。设置为 0 时表示无限制。
  • _transient_vacuum()
    清理临时记录。
    • 当 _transient_max_count 或 _transient_max_hours 条件满足时,会删除旧记录。
    • 实际清理操作每 5 分钟执行一次,因此该方法可以在需要时频繁调用(例如每次创建新记录时)。

示例(同时启用 max_hours 和 max_count):

  • 假设 max_hours = 0.2(12 分钟),max_count = 20,表中有 55 行记录:
    1. 有 10 行记录在过去 5 分钟内创建/更改。
    2. 有 12 行记录在 5 到 10 分钟内创建/更改。
    3. 剩余记录在 12 分钟前创建/更改。
  • 基于时间的清理:保留 12 分钟内创建/更改的 22 行记录。
  • 基于数量的清理:从剩余记录中再删除 12 行,而不是仅删除 2 行(否则每次新增记录都会立即触发清理)。
  • 注意:最近 5 分钟内创建/更改的 10 行记录不会被删除。

使用场景建议
  • 如果需要存储长期使用的业务数据,应选择 Model。
  • 如果需要创建一个逻辑类或共享代码,应选择 AbstractModel。
  • 如果需要处理临时数据(如向导或临时表单数据),应选择 TransientModel。

Fields类

odoo.fields.Field 是一个字段描述符,包含字段定义并管理记录上该字段的访问和赋值操作。以下参数可以在实例化字段时提供: 

参数说明:

  1. string (str)
    • 字段的显示标签。
    • 如果未设置,ORM 会使用类中的字段名称,并自动将其首字母大写作为标签。
  2. help (str)
    • 用户看到的字段工具提示信息。
  3. readonly (bool)
    • 是否为只读字段(默认:False)。
    • 仅影响用户界面(UI),在代码中仍然可以为字段赋值(只要字段是存储字段或可逆字段)。
  4. required (bool)
    • 是否为必填字段(默认:False)。
  5. index (str)
    • 决定字段是否在数据库中建立索引,以及索引的类型:
      • "btree" 或 True:标准索引,适合 many2one。
      • "btree_not_null":不包含 NULL 值的 BTREE 索引(适用于大部分值为 NULL 或永远不会查询 NULL 的情况)。
      • "trigram":基于三元组的通用倒排索引(适合全文搜索)。
      • None 或 False:不建立索引(默认)。
    • 注意:非存储字段或虚拟字段无法设置索引。
  6. default (value 或 callable)
    • 字段的默认值。可以是静态值,也可以是一个函数(该函数接收记录集作为参数并返回值)。
    • 如果需要避免字段设置默认值,请使用 default=None。
  7. groups (str)
    • 用逗号分隔的组 XML ID 列表(字符串)。
    • 限制字段访问权限,仅允许指定组的用户访问。
  8. company_dependent (bool)
    • 是否为公司相关字段。
    • 值以 jsonb 字典的形式存储在模型表中,以公司 ID 作为键。
    • 在 jsonb 字典中未指定的值,会回退使用 ir.default 模型中存储的默认值。
  9. copy (bool)
    • 在记录复制时,字段值是否也被复制:
      • 对于普通字段,默认值为 True。
      • 对于 one2many 和计算字段(包括属性字段和相关字段),默认值为 False。
  10. store (bool)
    • 字段值是否存储在数据库中:
      • 默认值为 True。
      • 对于计算字段,默认值为 False。
  11. aggregator (str)
    • 使用 read_group() 进行分组时的聚合函数,支持以下函数:
      • array_agg:将所有值(包括 NULL)连接为一个数组。
      • count:计数所有行。
      • count_distinct:计数不同的行数。
      • bool_and:所有值为真时返回真,否则返回假。
      • bool_or:至少一个值为真时返回真,否则返回假。
      • max:返回最大值。
      • min:返回最小值。
      • avg:返回平均值。
      • sum:返回总和。
  12. group_expand (str)
    • 当对当前字段分组时,用于扩展 read_group 结果的函数:
      • 对于选择字段,自动扩展为所有选择键。
    • 示例:
      @api.model
      def _read_group_selection_field(self, values, domain, order):
          return ['choice1', 'choice2', ...]  # 可用的选择项。
      
      @api.model
      def _read_group_many2one_field(self, records, domain, order):
          return records + self.search([custom_domain])  # 自定义扩展逻辑。

总结

上诉体现了odoo字段高度灵活性以及其强大功能,开发者可以根据实际业务需求自定义字段属性,优化数据存储和性能。

1. 字段定义的核心参数

  • string 和 help:优化用户界面,提供清晰的标签和提示信息。
  • readonly 和 required:控制字段的交互性和必填状态。
  • index:决定字段的数据库性能,尤其是需要快速查询时应合理设置索引。

2. 数据存储与复制行为

  • store 和 copy
    • 对于频繁查询的字段,设置 store=True 将数据存储在数据库中,提升查询性能。
    • copy 用于控制字段在复制记录时是否保留值。

3. 分组与聚合

  • aggregator 和 group_expand
    • 在报表或列表视图中通过 read_group() 实现分组和统计时,可根据需求指定聚合方式。
    • 例如,使用 sum 汇总某个字段值或 count_distinct 统计唯一值。

4. 公司相关字段

  • company_dependent
    • 适合多公司环境,允许每家公司为某个字段定义不同的值,而不需要为每家公司额外创建记录。

5. 示例应用场景

  • 设置一个只读字段:
    field_name = fields.Char(string="Example", readonly=True)
    
  • 添加分组统计字段:
    total_sales = fields.Float(string="Total Sales", aggregator="sum", store=True)
    
  • 定义公司相关的动态字段:
    tax_rate = fields.Float(string="Tax Rate", company_dependent=True)
    

计算字段(Computed Fields)

参数:

  1. compute (str)
    • 计算字段的方法名称。
    参见高级字段/计算字段
  2. precompute (bool)
    • 是否在记录插入数据库之前计算字段。
    • 当字段可以在记录插入之前计算时,应手动设置为 precompute=True(例如,避免依赖 search/read_group 的统计字段,或用于链接到上一条记录的 many2one 字段)。默认值为 False。
    注意:
    • 如果通过 create() 提供了显式值或默认值,预计算将不会执行,即使字段指定了 precompute=True。
    • 如果记录不是批量创建,而是一条一条插入,预计算可能适得其反。非预计算字段通常会在 flush() 阶段批量计算,并利用预取机制提升效率。而预计算字段会逐条计算,无法利用批量处理的性能优势。
    • 对于 one2many 类型的字段(通常由 ORM 自动批量创建),预计算可能更有意义。
  3. compute_sudo (bool)
    • 是否以超级用户身份重新计算字段以绕过访问权限。
      • 对于存储字段,默认值为 True。
      • 对于非存储字段,默认值为 False。
  4. recursive (bool)
    • 是否允许字段有递归依赖关系(例如字段依赖 X.parent_id.X)。
    • 必须显式声明字段为递归,以确保重新计算的正确性。
  5. inverse (str)
    • 定义字段反向操作的方法名称(可选)。
  6. search (str)
    • 定义字段自定义搜索逻辑的方法名称(可选)。
  7. related (str)
    • 用于声明相关字段的字段名序列。
  8. default_export_compatible (bool)
    • 是否在导出时默认包含该字段,并且该字段在导入时兼容。
    参见高级字段/相关字段

基础字段(Basic Fields)

fields. Boolean 类
  • 封装布尔值(bool)。
fields. Char 类
  • 基础字符串字段,可以设置长度限制,通常显示为单行字符串。

参数:

  1. size (int)
    • 存储在字段中的最大长度。
  2. trim (bool)
    • 是否自动修剪(默认:True)。
    • 注意,修剪操作仅在 Web 客户端中执行。
  3. translate (bool or callable)
    • 是否启用字段值的翻译;也可以是一个可调用对象,以通过 translate(callback, value) 实现字段翻译。
fields. Float 类
  • 封装浮点数(float)。

参数:

  1. digits (tuple(int, int) or str)
    • 定义浮点数精度的元组 (总位数, 小数位数),或引用 DecimalPrecision 记录的字符串。

工具方法:

  1. round():根据给定精度对浮点数进行四舍五入。
  2. is_zero():检查浮点数是否在给定精度下等于零。
  3. compare():比较两个浮点数的大小,支持以下语义:
    • 如果结果为 0,则两者相等;
    • 如果结果为 < 0,则第一个值小于第二个值;
    • 如果结果为 > 0,则第一个值大于第二个值。

示例:

  1. 对数量使用单位精度进行四舍五入:
    fields.Float.round(self.product_uom_qty, precision_rounding=self.product_uom_id.rounding)
    
  2. 检查数量是否为零:
    fields.Float.is_zero(self.product_uom_qty, precision_rounding=self.product_uom_id.rounding)
    
  3. 比较两个数量:
    if fields.Float.compare(self.product_uom_qty, self.qty_done, precision_rounding=self.product_uom_id.rounding) < 0:
        print("product_uom_qty is less than qty_done")
    
fields. Integer 类
  • 封装整型值(int)。
odoo.fields.Binary(二进制字段)

描述:用于封装二进制内容(例如文件)。

参数:

  • attachment:决定字段内容是存储为 ir.attachment 记录(默认值为 True)还是直接存储在模型的数据库表列中。
odoo.fields.Html(HTML字段)

描述:用于封装 HTML 内容。

参数:

  • sanitize:是否清理 HTML 内容(默认值为 True)。
  • sanitize_overridable:是否允许用户(属于特定用户组 base.group_sanitize_override)绕过 HTML 清理(默认值为 False)。
  • sanitize_tags:是否清理 HTML 标签,仅允许白名单中的标签属性(默认值为 True)。
  • sanitize_attributes:是否清理 HTML 属性,仅允许白名单中的属性(默认值为 True)。
  • sanitize_style:是否清理样式属性(默认值为 False)。
  • sanitize_conditional_comments:是否删除条件注释(默认值为 True)。
  • sanitize_output_method:使用 HTML 还是 XHTML 格式进行清理(默认值为 html)。
  • strip_style:是否删除样式属性而不进行清理(默认值为 False)。
  • strip_classes:是否删除类属性(默认值为 False)。
odoo.fields.Image(图像字段)

描述:继承自 Binary,用于封装图像。

参数:

  • max_width:图像的最大宽度(默认值为 0,即无限制)。
  • max_height:图像的最大高度(默认值为 0,即无限制)。
  • verify_resolution:是否验证图像分辨率以确保其未超过最大限制(默认值为 True,最大分辨率为 50e6 像素)。

注意:

如果未指定 max_width 和 max_height,且 verify_resolution 为 False,则应使用普通的 Binary 字段而非 Image 字段。

odoo.fields.Monetary(货币字段)

描述:用于封装以某种货币表示的浮点值。

参数:

  • currency_field:Many2one 字段的名称,指明此货币字段所使用的货币(默认值为 'currency_id')。
odoo.fields.Selection(选择字段)

描述:封装可选值的字段,支持不同选项的排他性选择。

参数:

  • selection:指定字段的可选值,可以是以下形式之一:
    1. 列表:包含 (value, label) 元组的列表,例如 [('a', 'Option A'), ('b', 'Option B')]。
    2. 回调方法:返回选项列表的方法。
    3. 方法名:模型中的方法名称。
  • selection_add:扩展现有选择值,用于覆盖字段的情况下。例如:
    selection = [('a', 'A'), ('b', 'B')]
    selection_add = [('c', 'C'), ('b',)]
    # 最终结果: [('a', 'A'), ('c', 'C'), ('b', 'B')]
    
  • ondelete:为 selection_add 提供的选项定义回退机制,其值为字典,支持以下行为:
    • 'set null':选项值将被设为 False(默认行为)。
    • 'cascade':选项及其对应的记录会被删除。
    • 'set default':选项值被设为字段的默认值。
    • 'set VALUE':选项值被设为指定的值。
    • 自定义回调函数:接收记录集,执行自定义处理。
odoo.fields.Text(文本字段)

描述:与 Char 类似,但用于更长的内容。没有长度限制,通常显示为多行文本框。

参数:

  • translate:是否支持字段值的翻译(默认值为 True)。如果为回调函数,则通过调用 translate(callback, value) 来获取翻译内容。
odoo.fields.Date (日期字段)和odoo.fields.Datetime(时间字段)

1. Date 和 Datetime 字段的重要性

日期和时间是任何业务应用中至关重要的字段。不正确的使用可能会导致隐蔽但棘手的问题。

赋值方式

Date 和 Datetime 字段可以通过以下方式赋值:

  • Python 对象:date 或 datetime。
  • 字符串格式
    • 对于 Date 字段:YYYY-MM-DD。
    • 对于 Datetime 字段:YYYY-MM-DD HH:MM:SS。
  • 特殊值:False 或 None(用于表示空值)。

辅助方法

Date 和 Datetime 字段的类提供了用于转换为兼容类型的辅助方法:

  • to_date():将值转换为 datetime.date。
  • to_datetime():将值转换为 datetime.datetime。

示例:

解析来自外部来源的日期或时间:

fields.Date.to_date(self._context.get('date_from'))

日期/时间比较的最佳实践

  • Date 字段:只能与 date 对象比较。
  • Datetime 字段:只能与 datetime 对象比较。

警告:

字符串形式的 Date 和 Datetime 是可以比较的,但不推荐,因为比较结果可能与预期不符。例如:

"2024-01-01 00:00:00" > "2024-01-01"  # 总是为 True

常见操作

Odoo 提供了便捷方法用于日期和时间的加减操作,以及获取特定周期的起始或结束时间。这些操作通过 odoo.tools.date_utils 提供的工具实现。

日期/时间操作方法:

  1. start_of(value, granularity)
    • 用于获取指定日期/时间的起始时间。
    • 参数
      • value: 初始日期或时间。
      • granularity: 周期类型,可选值包括 'year', 'quarter', 'month', 'week', 'day', 'hour'。
    • 返回值:返回起始日期/时间。
  2. end_of(value, granularity)
    • 用于获取指定日期/时间的结束时间,参数和返回值与 start_of 类似。
  3. add(value, *args, **kwargs)
    • 用于对日期/时间执行加操作。
    • 参数
      • value: 初始日期/时间。
      • *args: 传递给 relativedelta 的参数。
      • **kwargs: 额外的关键字参数传递给 relativedelta。
    • 返回值:计算后的日期/时间。
  4. subtract(value, *args, **kwargs)
    • 用于对日期/时间执行减操作,参数和返回值与 add 类似。

其他工具方法

  1. today()
    • 返回当天的日期(格式适合 ORM 使用)。
    • 可用作计算字段的默认值。
  2. context_today(record, timestamp=None)
    • 返回当前日期(客户端时区)。
    • 参数
      • record: 从中获取时区的记录集。
      • timestamp: 可选,替代当前时间的时间值(必须为 datetime 类型)。
  3. to_date(value)
    • 将值尝试转换为 date 对象,若为 datetime 则丢失时间信息(时、分、秒等)。
    • 参数:value(字符串、date 或 datetime)。
    • 返回值:date 或 None。
  4. to_string(value)
    • 将 date 或 datetime 转换为字符串,格式适合服务器。
    • 返回值:如果是 datetime,则时间部分会被截断(00:00:00)。

时间字段与时区

  1. 存储机制
    • Datetime 字段在数据库中以 UTC 时区存储。这使数据库不依赖托管服务器的时区。
    • 时区的转换完全由客户端负责。
  2. 方法:context_timestamp(record, timestamp)
    • 将给定时间戳转换为客户端时区。
    • 注意
      • 适用于特定时间展示,但不适合作为字段的默认值。
      • 默认值建议使用 now()。

应用场景总结

  • 日期字段:适合纯日期(无时间)操作。
  • 时间字段:适合需要处理时区的日期时间操作。
  • 时间工具:如加减运算、周期计算,便于开发时处理复杂业务逻辑。

关系字段

1. 基础概念

在 Odoo 中,关系字段分为以下几种类型,用于定义模型间的关系:

  • Many2one:表示当前模型到目标模型的单向多对一关系。
  • One2many:表示当前模型到目标模型的单向一对多关系。
  • Many2many:表示两个模型之间的多对多关系。
  • Reference 和 Many2oneReference:伪关系字段,数据存储不使用外键。

每种字段有不同的参数和使用场景,以下是详细介绍。

2. Many2one 字段

含义

  • Many2one 字段的值是一个大小为 0(无记录)或 1(单个记录)的记录集。

参数

  • comodel_name(必填):目标模型的名称。
  • domain(可选):用于客户端筛选候选值的可选域表达式,可以是固定的域或动态 Python 表达式。
  • context(可选):用于客户端操作该字段时的上下文。
  • ondelete:指定目标记录被删除时的处理方式:
    • 'set null':将字段值设置为 Null。
    • 'restrict':禁止删除目标记录。
    • 'cascade':删除目标记录时,当前模型的相关记录一并删除。
  • auto_join:是否在搜索时自动生成 SQL JOIN(默认为 False)。
  • delegate:设为 True 时,当前模型可以直接访问目标模型的字段(相当于 _inherits)。
  • check_company:启用公司约束,确保字段指向的记录与当前记录的公司一致。

3. One2many 字段

含义

  • One2many 字段的值是一个记录集,包含所有满足以下条件的目标记录:
    • 目标模型中 Many2one 字段的值等于当前记录。

参数

  • comodel_name(必填):目标模型的名称。
  • inverse_name(必填):目标模型中与当前模型关联的 Many2one 字段的名称。
  • 其他参数如 domain、context 和 auto_join 的意义与 Many2one 字段相同。

4. Many2many 字段

含义

  • Many2many 字段的值是一个记录集,表示两个模型间的多对多关系。

参数

  • comodel_name(必填):目标模型的名称。
  • relation(可选):存储关系的中间表的名称,若未提供,Odoo 会根据模型名称自动生成。
  • column1column2(可选):中间表中分别引用当前模型和目标模型记录的列名。
  • 其他参数如 domain、context 和 check_company 的意义与 Many2one 字段相同。

注意事项

  • 如果多个 Many2many 字段未显式定义 relation,且使用相同的目标模型,ORM 将报错。
  • 解决方式:
    • 显式定义 relation。
    • 使模型的 _auto 属性为 False。

5. Command 方法

One2many 和 Many2many 字段支持使用 Command 类来操作其关系。每个命令是一个包含三部分的元组:

  1. 命令标识符(整数)。
  2. 记录 ID 或 0(取决于命令类型)。
  3. 用于更新或创建记录的值,或新关联记录的列表,或 0。

命令类型

  • CREATE (0):创建新记录,并与当前记录关联。
  • UPDATE (1):更新已关联记录的字段值。
  • DELETE (2):删除记录,并移除关联。
  • UNLINK (3):仅移除关联,不删除目标记录。
  • LINK (4):将已有记录与当前记录关联。
  • CLEAR (5):移除所有关联。
  • SET (6):替换当前关联记录。

示例

# 添加一条新记录
Command.create({'name': 'New Item'})

# 更新已有记录
Command.update(3, {'name': 'Updated Name'})

# 删除记录并移除关联
Command.delete(3)

# 仅移除关联
Command.unlink(3)

# 替换关联
Command.set([1, 2, 3])

6. Reference 和 Many2oneReference 字段

Reference

  • 存储值为字符串,格式为 "res_model,res_id"。
  • 不建立数据库外键。

Many2oneReference

  • 存储值为目标记录的整数 ID。
  • 目标模型的名称需要存储在一个 Char 字段中,并通过 model_field 参数指定该字段的名称。

保留字段的名称

1. 计算字段 (Computed Fields)

定义

计算字段是动态计算值的字段,而不是直接从数据库中读取。通过参数 compute 指定计算方法。例如:

from odoo import api, fields, models

class MyModel(models.Model):
    _name = 'my.model'

    total = fields.Float(compute='_compute_total')

    @api.depends('value', 'tax')
    def _compute_total(self):
        for record in self:
            record.total = record.value + record.value * record.tax

依赖声明

  • 如果计算字段依赖其他字段,需要通过 @api.depends 装饰器声明这些字段。
  • 依赖字段可以是嵌套字段(如 line_ids.value)。

存储

  • 默认情况下,计算字段不存储,每次访问时动态计算。
  • 设置 store=True 可以将其存储到数据库中,同时允许搜索。

搜索

  • 计算字段默认不可搜索。
  • 使用 search 参数指定搜索方法,返回等效的搜索域:
upper_name = fields.Char(compute='_compute_upper', search='_search_upper')

def _search_upper(self, operator, value):
    if operator == 'like':
        operator = 'ilike'
    return [('name', operator, value)]

只读

  • 默认情况下,计算字段是只读的。
  • 可通过 inverse 参数设置逆运算方法,使字段值可写:
document = fields.Char(compute='_get_document', inverse='_set_document')

def _get_document(self):
    for record in self:
        with open(record.get_document_path()) as f:
            record.document = f.read()

def _set_document(self):
    for record in self:
        if record.document:
            with open(record.get_document_path(), 'w') as f:
                f.write(record.document)

多个字段的计算

  • 同一个方法可以计算多个字段:
discount_value = fields.Float(compute='_apply_discount')
total = fields.Float(compute='_apply_discount')

@api.depends('value', 'discount')
def _apply_discount(self):
    for record in self:
        discount = record.value * record.discount
        record.discount_value = discount
        record.total = record.value - discount

注意事项

  • 不建议为多个字段使用同一个逆运算方法,否则可能导致数据不一致。

2. 相关字段 (Related Fields)

定义

相关字段是特殊的计算字段,用于从当前记录的子字段中获取值。例如:

nickname = fields.Char(related='user_id.partner_id.name', store=True)

特性

  • 默认不存储、不复制,只读,超管模式计算。
  • 设置 store=True 可存储并允许搜索。

依赖声明

  • 使用 depends 参数指定精确依赖,避免因子字段变化导致不必要的重新计算:
nickname = fields.Char(related='partner_id.name', store=True, depends=['partner_id'])

限制

  • 不支持链式依赖 Many2many 或 One2many 的子字段。

3. 自动字段 (Automatic Fields)

Odoo 自动为模型生成以下字段,若 _log_access 属性启用,则会自动记录创建和更新信息:

访问日志字段

  • create_date:记录创建时间。
  • create_uid:记录创建者(res.users 的 Many2one)。
  • write_date:记录最近更新时间。
  • write_uid:记录最近更新者。

启用与禁用

  • 默认 _log_access 和 _auto 均为 True。
  • 禁用 _log_access 可以避免生成这些字段。

4. 保留字段 (Reserved Fields)

以下字段名称具有特定用途,定义时需遵守相关行为约定:

常见保留字段

  1. name
    • 用作 _rec_name 的默认值,表示记录的主要名称。
  2. active
    • 控制记录的全局可见性,默认为 True。设置为 False 时,记录在大多数搜索和列表中不可见。
    • 提供的方法:
      • toggle_active():反转 active 值。
      • action_archive():设置 active=False(归档)。
      • action_unarchive():设置 active=True(取消归档)。
  3. state
    • 用于表示对象的生命周期阶段,通常是 Selection 字段。
  4. parent_id
    • 组织记录为树形结构时的父级记录字段。
    • 配合 _parent_store=True 和 parent_path 优化 child_of、parent_of 操作。
  5. company_id
    • 用于多公司行为检查,定义记录的归属公司。

Recordsets(记录集)

定义

记录集是同一模型的记录的有序集合,用于在 Odoo 中与模型和记录进行交互。

警告

尽管名字中包含 “sets” 一词,但记录集中可能包含重复项。未来版本可能会对此进行更改。

特点
  • 记录集的方法是在记录集范围内执行的,self 表示当前记录集。
  • 记录集可以包含 0 到 N 条记录,甚至是整个数据库的所有记录。
单记录迭代
  • 对记录集进行迭代时,会生成单条记录的记录集(“单例”)。

示例:

class AModel(models.Model):
    _name = 'a.model'

    def a_method(self):
        print(self)  # 输出记录集,比如 a.model(1, 2, 3)
        for record in self:
            print(record)  # 逐条输出单记录 a.model(1), a.model(2), ...

字段访问

直接访问字段
  • 记录集提供了“活动记录”接口,可以直接读取或写入字段值:
    record.name  # 读取字段
    record.name = "New Name"  # 写入字段
    
动态字段访问
  • 动态字段名称可以通过 getattr() 访问,更优雅且更安全:
    field_name = "name"
    print(record[field_name])  # 动态访问字段
    
非关系字段限制
  • 如果记录集包含多条记录,读取非关系字段会抛出错误。
  • 关系字段(Many2one、One2many、Many2many)总是返回记录集,即使字段为空。
批量字段访问
  • 对多条记录批量获取字段值时,使用 mapped() 方法:
    total_qty = sum(self.mapped('qty'))  # 获取所有记录的 qty 总和
    

记录缓存与预取(Prefetching)

缓存机制

Odoo 使用缓存机制来避免每次访问字段时都查询数据库:

  • 第一次访问字段时从数据库读取值。
  • 后续访问直接从缓存中获取。

示例:

record.name  # 第一次访问,从数据库读取
record.name  # 第二次访问,从缓存读取
预取机制
  • 为提高性能,Odoo 会根据某些启发式规则进行字段和记录的预取操作。
  • 当访问某记录的某字段时,Odoo 实际上会从该记录集预取更多字段,并缓存这些值。

示例:

for partner in partners:
    print(partner.name)  # 第一次访问时预取所有 'name' 和 'lang' 字段
    print(partner.lang)  # 缓存中直接获取
  • 如果访问了关联字段(如 Many2one),相关模型的记录也会被订阅以便未来预取。
  • 例如:
    countries = set()
    for partner in partners:
        country = partner.country_id  # 首次预取所有关联国家
        countries.add(country.name)  # 再次访问时从缓存中获取
    
预取优势
  • 例如在有 1000 条记录的循环中:
    • 如果没有预取,每次查询都会触发 2000 次数据库查询。
    • 有预取时,仅需 1 次查询。
辅助方法
  • search_fetch() 和 fetch() 方法可以手动填充记录的缓存,适用于预取机制不足的场景。

Odoo 方法装饰器(Method decorators)

Odoo 提供了一些方法装饰器,用于简化和增强模型方法的功能。

1. @api.model
  • 作用:用于装饰记录风格的方法。在这种方法中,self 是一个记录集,但记录集的内容不重要,只关心模型本身。

示例:

@api.model
def method(self, args):
    ...
  • 解读:此装饰器用于那些与模型本身相关,但不依赖于特定记录的操作。
2. @api.constrains(*args)
  • 作用:用于定义约束检查方法,检查一个或多个字段的条件。每个参数必须是一个字段名。

示例:

@api.constrains('name', 'description')
def _check_description(self):
    for record in self:
        if record.name == record.description:
            raise ValidationError("Fields name and description must be different")
  • 解读:此装饰器用于约束方法,当指定字段发生更改时触发。若验证失败,应抛出 ValidationError。
  • 注意:@constrains 只支持简单字段名,关系字段(如 partner_id.customer)将被忽略。
3. @api.depends(*args)
  • 作用:指定计算字段的方法依赖的字段。每个参数必须是字段名(可以是以点分隔的关系字段)。

示例:

@api.depends('partner_id.name', 'partner_id.is_company')
def _compute_pname(self):
    for record in self:
        if record.partner_id.is_company:
            record.pname = (record.partner_id.name or "").upper()
        else:
            record.pname = record.partner_id.name
  • 解读:该装饰器用于指定计算字段的依赖关系,确保当依赖字段的值发生变化时,计算字段会自动更新。
4. @api.onchange(*args)
  • 作用:用于装饰当字段值发生变化时被调用的方法。每个参数必须是一个字段名。

示例:

@api.onchange('partner_id')
def _onchange_partner(self):
    self.message = "Dear %s" % (self.partner_id.name or "")
    return {
        'warning': {'title': "Warning", 'message': "What is this?", 'type': 'notification'},
    }
  • 解读:该装饰器会在指定字段变化时触发。方法返回的内容会自动发送回客户端,显示通知或警告。
  • 警告:@onchange 只支持简单字段名,不支持关系字段(如 partner_id.tz)。
5. @api.returns(model, downgrade=None, upgrade=None)
  • 作用:装饰器用于指定方法的返回值模型。downgrade 和 upgrade 参数用于将记录风格和传统风格的输出进行转换。

示例:

@api.returns('res.partner')
def find_partner(self, arg):
    ...
  • 解读:该装饰器用于方法返回记录风格的实例,downgrade 和 upgrade 可以用于转换返回值的格式。
6. @api.autovacuum(method)
  • 作用:装饰一个方法,使其在每天的自动清理任务中被调用,通常用于垃圾回收类的任务。
7. @api.depends_context(*args)
  • 作用:用于指定非存储“计算”字段的方法的上下文依赖关系。每个参数是上下文字典中的键。

示例:

@api.depends_context('pricelist')
def _compute_product_price(self):
    for product in self:
        pricelist = self.env['product.pricelist'].browse(self.env.context['pricelist'])
        product.price = pricelist._get_products_price(product).get(product.id, 0.0)
  • 解读:此装饰器指定计算字段依赖于上下文中的值。
8. @api.model_create_multi(method)
  • 作用:装饰一个方法,使其可以接受一个字典列表并创建多个记录。方法可以通过单个字典或字典列表调用。

示例:

python复制代码record = model.create(vals)
records = model.create([vals, ...])
  • 解读:该装饰器用于批量创建记录,允许同时处理多个字典数据。
9. @api.ondelete(*args, at_uninstall)
  • 作用:装饰一个方法,在删除记录时触发,通常用于防止删除某些特定记录(如已验证的销售订单)时抛出错误。at_uninstall 参数控制是否在卸载模块时触发该方法。

示例:

@api.ondelete(at_uninstall=False)
def _unlink_if_user_inactive(self):
    if any(user.active for user in self):
        raise UserError("Can't delete an active user!")
  • 解读:该装饰器用于防止删除某些记录。例如,阻止删除处于活动状态的用户。at_uninstall 默认是 False,意味着只有在通过 unlink 删除时才会触发。如果设置为 True,则在模块卸载时也会触发。
总结

这些装饰器为 Odoo 提供了强大的方法修饰功能。通过这些装饰器,可以轻松地实现如下功能:

  1. 模型方法定义 (@api.model),
  2. 约束验证 (@api.constrains),
  3. 字段计算 (@api.depends, @api.depends_context),
  4. 字段变化监听 (@api.onchange),
  5. 方法返回值处理 (@api.returns),
  6. 自动清理 (@api.autovacuum),
  7. 批量创建记录 (@api.model_create_multi),
  8. 删除记录时的验证 (@api.ondelete).



未完待续->  odoo-ORM2 | 同欣数字化落地






 


徐朋朋 2024年11月22日
分析这篇文章

存档
登录 留下评论
第七章:模型间关系