第二章 模型(Models)

篇别:第一篇 基础知识

本章学习目标

  • 能正确声明 Model / AbstractModel / TransientModel
  • 能组合 字段、约束、索引 声明方式(含 Odoo 19 习惯用法)。
  • 能说出跨版本 模型重构 时自定义模块的应对步骤。

2.1 模型基础

2.1.1 知识要点

  • models.Model:持久化业务实体,_name 全局唯一(点分命名,如 library.book)。
  • models.AbstractModel_abstract = True,不创建业务表,供继承复用。
  • models.TransientModel:临时表,适合向导;系统会清理过期数据。

2.1.2 案例

案例 A:最小模型

from odoo import models, fields

class Book(models.Model):
    _name = 'library.book'
    _description = '图书'
    name = fields.Char(required=True)
    isbn = fields.Char()

案例 B:抽象基类供多模型继承

class TimestampMixin(models.AbstractModel):
    _name = 'library.timestamp.mixin'
    _description = '时间戳混入'
    create_date = fields.Datetime(readonly=True)
    # 子模型 _inherit 此 mixin 即可获得统一字段(若需自定义 create_date 需另设计)

说明:create_date 通常由框架提供,此处仅示意 mixin 模式。

2.1.3 截图占位

图 2-1 设置 → 技术 → 模型:搜索 library.book,查看模型与表名

2.1.4 本节练习

  1. 填空:普通业务单据应继承自 Odoo 的 __ 类。
  2. 判断_description 会影响数据库表名。( )
  3. 实操:新建模型 training.course,字段 namehours(浮点)。

参考答案提示:1. models.Model。2. 错(表名由 _name 等规则生成)。3. 见案例 A 扩展。


2.2 字段定义

2.2.1 知识要点

  • 标量CharTextIntegerFloatBooleanDateDatetimeMonetaryHtmlBinarySelection
  • 关系Many2one(多对一)、One2many(一对多,反向字段)、Many2many
  • 计算compute= + @api.depends;可选 storeinverserelated

2.2.2 案例

案例 A:订单行与产品

class OrderLine(models.Model):
    _name = 'training.order.line'
    order_id = fields.Many2one('training.order', required=True, ondelete='cascade')
    product_id = fields.Many2one('product.product', string='产品')
    qty = fields.Float(default=1.0)

class Order(models.Model):
    _name = 'training.order'
    line_ids = fields.One2many('training.order.line', 'order_id', string='明细')

案例 B:计算总价

amount_total = fields.Monetary(compute='_compute_total', currency_field='currency_id', store=True)

@api.depends('line_ids.subtotal')
def _compute_total(self):
    for order in self:
        order.amount_total = sum(order.line_ids.mapped('subtotal'))

2.2.3 截图占位

图 2-2 同一模型在「字段」技术列表中的类型与关系 comodel

2.2.4 本节练习

  1. 选择题One2many 必须配合对方模型上的( )字段。(A)Many2many (B)Many2one (C)Reference
  2. 简答Monetary 为什么需要 currency_field
  3. 实操:为 training.course 增加 Many2one 指向 res.partner(讲师)。

参考答案提示:1. B。2. 金额需按币种格式化与换算。3. instructor_id = fields.Many2one('res.partner')


2.3 约束与索引(补充)

2.3.1 知识要点

  • SQL 约束_sql_constraints 或新版本推荐的模型级约束声明(以当前源码 odoo.models 为准)。
  • Python 约束@api.constrains 做跨字段逻辑校验。
  • 索引index=True 或模型级索引定义,优化常用 domain 字段。

2.3.2 案例

案例:唯一 ISBN

_sql_constraints = [
    ('book_isbn_uniq', 'unique(isbn)', 'ISBN 不能重复!'),
]

@api.constrains('start_date', 'end_date')
def _check_dates(self):
    for rec in self:
        if rec.end_date and rec.start_date and rec.end_date < rec.start_date:
            raise ValidationError('结束日期不能早于开始日期')

2.3.3 截图占位

图 2-3 升级模块后 PostgreSQL 中该表约束(\d table_name)

2.3.4 本节练习

  1. 简答:何时优先用 SQL 约束而非 @api.constrains
  2. 实操:为 training.coursecode 字段加唯一约束。

参考答案提示:1. 简单唯一/检查、需数据库层强制与并发安全时。2. _sql_constraints 或 ORM 等价 API。


2.4 模型重构指南(新增)

2.4.1 知识要点

大版本升级时官方可能 合并模型、重命名字段(HR、库存 UoM、会计 EDI 等)。自定义模块应:

  1. 阅读 Release Notes 与升级指南。
  2. 使用 upgrade_code / 迁移脚本 替换废弃符号。
  3. 避免依赖已删除的 _name

2.4.2 案例

案例:字段改名迁移思路

  • 旧字段 product_uom → 新 product_uom_id
  • migrations/19.0.1.0.0/pre-migrate.py(或项目约定路径)中用 SQL ALTER TABLE ... RENAME COLUMN,或在 post_init 中数据拷贝(大表慎用)。

2.4.3 截图占位

图 2-4 官方文档 ORM Changelog 中「模型/字段重命名」片段

2.4.4 本节练习

  1. 简答:升级前为什么要先在 数据库副本 上跑 -u
  2. 拓展:在 Changelog 中找一条与你业务相关的废弃项并写出应对句。

参考答案提示:1. 避免直接破坏生产数据与不可回滚结构变更。


本章综合练习

  1. 实现:training.session(TransientModel)批量创建 training.course
  2. 说明:store=True 的计算字段在搜索与性能上的利弊。

本章对应白皮书目录:第二章 模型。