Odoo 17 的 API 和 CRUD 详解:掌握数据操作的核心

在 Odoo 开发的广阔世界里,一切都围绕着数据展开。无论是创建一个销售订单,更新客户信息,还是读取产品库存,我们始终在与数据库中的记录打交道。为了规范和简化这些交互,Odoo 提供了一套强大而优雅的工具:API 装饰器ORM 方法

理解并熟练运用这两者,是衡量一位 Odoo 开发者水平的关键标尺。本文将深入解析 Odoo 17 中最核心的 API 装饰器和标准的 CRUD(创建、读取、更新、删除)操作。

核心概念:self 是什么?

在开始之前,必须理解 self。在 Odoo 的模型方法中,self 不是指单个对象,而是一个 记录集(Recordset)——一个包含了零个、一个或多个记录的有序集合。例如,self 可能代表一个选中的销售订单,也可能代表所有状态为“草稿”的订单。理解这一点是掌握后续内容的基础。

第一部分:Odoo API 装饰器 - 行为的指挥官

API 装饰器(Decorators)是附加在方法定义之上的特殊标记(如 @api.model)。它们不会改变方法内部的代码,但会改变方法的调用方式、self 的内容以及 Odoo 框架如何与之交互。

1. @api.model
  • 作用:定义一个“类级别”的方法。
  • self 的内容:一个不包含任何记录的空记录集(例如 sale.order())。方法内部不能依赖任何具体的记录数据。
  • 使用场景
    • 重写 create() 方法。
    • 定义不需要特定记录即可调用的工具函数。
    • 从 UI 调用一个不需要上下文记录的动作。

示例:重写 create,在创建伙伴时自动添加一个标签。

from odoo import models, fields, api

class ResPartner(models.Model):
    _inherit = 'res.partner'

    @api.model
    def create(self, vals):
        # 即使在创建单个伙伴时,self 也是空的 res.partner()
        # vals 是一个包含所有待创建字段值的字典
        tag_id = self.env.ref('my_module.default_partner_tag', raise_if_not_found=False)
        if tag_id:
            vals.setdefault('category_id', [(4, tag_id.id)])
        
        # 调用原始的 create 方法
        return super(ResPartner, self).create(vals)
2. @api.onchange('field_name_1', 'field_name_2', ...)
  • 作用:当表单视图中的指定字段发生变化时,在客户端(浏览器)实时触发此方法。
  • self 的内容:一个虚拟的、仅存在于内存中的单条记录。它的值是用户在表单上看到的值。
  • 关键点
    • 不写入数据库。它的目的是动态改变表单上其他字段的值、显示警告或更新域(domain)。
    • 它的返回值是一个字典,可以包含 {'warning': ...}{'domain': ...}

示例:当选择一个客户时,自动填充其默认的送货地址。

class SaleOrder(models.Model):
    _inherit = 'sale.order'

    @api.onchange('partner_id')
    def _onchange_partner_id(self):
        # self 是表单上正在编辑的单个销售订单的虚拟记录
        if self.partner_id:
            self.partner_shipping_id = self.partner_id.with_context(
                is_delivery=True
            ).address_get(['delivery'])['delivery']
3. @api.depends('field_name_1', 'field_name_2', ...)
  • 作用:定义一个**计算字段(Computed Field)**的计算逻辑。
  • self 的内容:一个包含一个或多个需要重新计算该字段值的记录集。
  • 关键点
    • 当依赖的字段发生变化时,此方法会被自动调用。
    • 方法必须遍历 self (通常是 for record in self:) 并为每条记录的计算字段赋值。

示例:计算订单行的总金额。

class SaleOrderLine(models.Model):
    _inherit = 'sale.order.line'

    # 定义计算字段
    price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal')

    # 定义计算方法,并用 @api.depends 装饰
    @api.depends('product_uom_qty', 'price_unit', 'discount')
    def _compute_amount(self):
        # self 可能包含多条订单行
        for line in self:
            price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
            line.price_subtotal = price * line.product_uom_qty
4. @api.constrains('field_name_1', ...)
  • 作用:定义数据库级别的验证约束
  • self 的内容:包含了被创建或被修改的记录的记录集。
  • 关键点
    • 在记录被写入数据库(createwrite)时触发。
    • 如果约束条件不满足,必须抛出 odoo.exceptions.ValidationError 异常。

示例:确保课程的结束日期不能早于开始日期。

from odoo.exceptions import ValidationError

class Course(models.Model):
    _name = 'my_module.course'

    start_date = fields.Date()
    end_date = fields.Date()

    @api.constrains('start_date', 'end_date')
    def _check_dates(self):
        for course in self:
            if course.start_date and course.end_date and course.start_date > course.end_date:
                raise ValidationError("课程的结束日期不能早于开始日期!")

第二部分:ORM 方法 - CRUD 操作的实践

Odoo 的对象关系映射(ORM)将数据库操作封装成了简单直观的方法。所有这些方法都通过 self.env['model.name'] 来访问。

C - Create (创建): create()
  • 方法: self.env['model.name'].create(vals)
  • vals: 一个字典,键是字段名,值是字段值。
  • 返回值: 一个包含了新创建记录的记录集。
# 创建一个新客户
partner_vals = {'name': '张三', 'email': '[email protected]'}
new_partner = self.env['res.partner'].create(partner_vals)
print(f"创建成功,新客户ID: {new_partner.id}")
R - Read (读取): search(), browse(), search_read()

1. search(domain, limit=None, order=None)

  • 作用:根据条件(domain)查找记录。
  • domain: 一个由元组组成的列表,用于过滤记录。例如 [('state', '=', 'sale')]
  • 返回值: 一个包含了符合条件记录的记录集
# 查找所有已确认的销售订单
sale_orders = self.env['sale.order'].search([('state', '=', 'sale')])
for order in sale_orders:
    print(order.name)

2. browse(ids)

  • 作用:根据已知的 ID 列表获取记录。
  • ids: 一个整数列表或元组。
  • 返回值: 一个包含了这些 ID 对应记录的记录集
# 获取 ID 为 1 和 5 的产品
products = self.env['product.product'].browse([1, 5])

3. search_read(domain, fields=None, ...)

  • 作用search()read() 的高效结合。直接搜索并返回记录的数据。
  • 返回值: 一个字典列表,而不是记录集。这在需要将数据传给外部系统或前端 JavaScript 时非常高效。
# 查找所有客户的名称和邮箱
partners_data = self.env['res.partner'].search_read(
    domain=[('is_company', '=', True)],
    fields=['name', 'email']
)
# partners_data 的结果: [{'id': 1, 'name': '公司A', 'email': '...'}, ...]
U - Update (更新): write()
  • 方法: recordset.write(vals)
  • 作用: 更新一个或多个记录。
  • vals: 一个包含要更新字段和新值的字典。
  • 返回值: True
  • 注意: write 是在记录集上调用的,可以同时更新多条记录。
# 将单个订单状态更新为“完成”
order = self.env['sale.order'].browse(10)
order.write({'state': 'done'})

# 将所有草稿状态的订单指派给当前用户
draft_orders = self.env['sale.order'].search([('state', '=', 'draft')])
draft_orders.write({'user_id': self.env.user.id})
D - Delete (删除): unlink()
  • 方法: recordset.unlink()
  • 作用: 从数据库中删除一个或多个记录。
  • 返回值: True
# 找到所有已取消的销售订单并删除它们
cancelled_orders = self.env['sale.order'].search([('state', '=', 'cancel')])
if cancelled_orders:
    cancelled_orders.unlink()

结论:融合 API 与 CRUD

在实际开发中,API 装饰器和 CRUD 方法总是协同工作。一个典型的业务逻辑可能是:

  1. @api.onchange 在用户界面提供即时反馈。
  2. 在用户点击“保存”时,create()write() 被调用。
  3. @api.constrains 装饰的方法在数据写入前进行最终验证。
  4. @api.depends 装饰的方法在相关数据更新后,自动重新计算字段值。
  5. 一个自定义的按钮动作(一个普通方法)可能会使用 search() 找到一批记录,然后调用 write()unlink() 对它们进行批量处理。

掌握 Odoo 17 的 API 和 CRUD,就等于掌握了与 Odoo 数据库对话的语言。这不仅能让你高效地实现业务需求,更能让你编写出结构清晰、性能优越、易于维护的健壮代码,从而真正释放 Odoo 框架的全部潜力。

你可能感兴趣的:(erp,API,Odoo,Odoo17)