第三章:模型与ORM深入

好,咱们进入第三章。这一章可以说是Odoo开发的核心中的核心。模型与ORM,说白了就是你跟数据库打交道的全部手段。我见过太多新手,写了一大堆业务逻辑,结果模型定义一塌糊涂,后面维护起来简直想哭。嗯,咱们今天就把这块彻底讲透。

3.1 模型继承机制

Odoo的模型继承,是我个人觉得最巧妙的设计之一。它不像传统开发那样,你得去改别人的源码。你想想看,要是你买了一套ERP,想加个字段就得改核心代码,那升级的时候怎么办?

Odoo提供了三种继承方式,咱们一个一个说。

3.1.1 经典继承(_inherit)

这是最常用的方式。你想在已有的模型上加字段、改方法,就用它。

from odoo import models, fields

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

    is_vip = fields.Boolean(string='VIP客户')
    vip_level = fields.Integer(string='VIP等级', default=1)

你看,就这么简单。我曾在项目中遇到一个需求:要在客户表单上加个「最后下单时间」。用_inherit,五分钟搞定,完全不影响原有逻辑。

我的习惯:继承时尽量只加自己的字段,不要轻易改已有字段的属性。除非你非常确定,否则容易出问题。

3.1.2 原型继承(_name + _inherit)

这种继承会创建一个全新的模型。它复制了父模型的所有字段和方法,但有自己的数据表。

class SaleOrder(models.Model):
    _name = 'sale.order.custom'
    _inherit = 'sale.order'
    _description = '自定义销售订单'

    custom_field = fields.Char('自定义字段')

为什么要这么干?我记得有一次,客户想要一个跟销售订单几乎一样,但业务逻辑完全不同的单据。用原型继承,既保留了原有功能,又独立存储,互不干扰。

3.1.3 委托继承(_inherits)

这个稍微复杂点,但非常强大。它允许一个模型「委托」给另一个模型,共享数据。

class CustomUser(models.Model):
    _name = 'custom.user'
    _inherits = {'res.users': 'user_id'}

    user_id = fields.Many2one('res.users', required=True, ondelete='cascade')
    department = fields.Char('部门')

说白了,就是你在custom.user里操作,数据会同步到res.users。我一般只在需要扩展系统核心模型,但又不想破坏原有结构时用这个。

避坑指南:我曾经在委托继承上栽过跟头。如果你用了_inherits,记得加上ondelete='cascade',否则删数据时会报错。嗯,血的教训。

3.2 字段类型详解

字段类型这块,看起来简单,但用好了能省不少事。咱们挑几个重点说说。

3.2.1 基础字段

字段类型 说明 我的经验
Char 字符串,最长255字符 记得加size限制,不然用户能输出一篇作文
Integer 整数 做数量、序号时首选
Float 浮点数 涉及金额时用Float,但要注意精度
Boolean 布尔值 开关、状态标记,简单直接
Date 日期 默认用fields.Date.today()

举个例子,一个完整的模型定义大概长这样:

class ProductTemplate(models.Model):
    _name = 'product.template'

    name = fields.Char('产品名称', required=True, size=100)
    code = fields.Char('产品编码', size=20)
    price = fields.Float('价格', digits=(10, 2))
    is_active = fields.Boolean('启用', default=True)
    release_date = fields.Date('发布日期', default=fields.Date.today)

3.2.2 关联字段

这才是Odoo的精髓。三个关联字段,搞懂了,你就能把数据串起来。

Many2one(多对一):最常见。比如一个订单属于一个客户。

partner_id = fields.Many2one('res.partner', string='客户', required=True)

One2many(一对多):一个订单有多个订单行。

order_line_ids = fields.One2many('sale.order.line', 'order_id', string='订单行')

Many2many(多对多):一个产品可以属于多个分类,一个分类也可以有多个产品。

category_ids = fields.Many2many('product.category', string='产品分类')
小技巧:Many2many字段名我习惯加_ids后缀,One2many也是。这样一看就知道是关联字段。团队协作时,命名规范能省很多沟通成本。

3.3 约束与默认值

约束,就是给数据上规矩。默认值,就是给用户省事。这两样用好了,数据质量能提升一大截。

3.3.1 SQL约束

直接在数据库层面做限制,效率最高。

_sql_constraints = [
    ('name_uniq', 'unique(name)', '产品名称不能重复!'),
    ('code_uniq', 'unique(code)', '产品编码已存在!'),
]

我一般把唯一性约束、非空约束放在SQL层面。为什么?因为数据库检查比Python快得多,而且不会因为代码bug而漏掉。

3.3.2 Python约束

复杂的业务逻辑,用Python约束。比如价格不能为负数:

from odoo.exceptions import ValidationError

@api.constrains('price')
def _check_price(self):
    for record in self:
        if record.price < 0:
            raise ValidationError('价格不能为负数!')
注意:Python约束只在调用create或write时触发。如果你直接操作数据库,约束不会生效。所以,永远不要绕过ORM直接写SQL。

3.3.3 默认值设置

默认值有三种常见写法:

  1. 静态默认值:default=True,default=0
  2. 函数默认值:default=_compute_default
  3. 关联默认值:default=lambda self: self.env.user.id

举个例子:

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

    user_id = fields.Many2one('res.users', string='销售员',
        default=lambda self: self.env.user)
    date_order = fields.Date('订单日期',
        default=fields.Date.context_today)
    state = fields.Selection([
        ('draft', '草稿'),
        ('confirm', '已确认'),
        ('done', '已完成')
    ], string='状态', default='draft')

你看,销售员默认是当前登录用户,日期默认是今天,状态默认是草稿。用户打开表单,什么都不用填,直接就能保存。这才是好的用户体验。

核心总结:

  • 继承用_inherit,别改核心代码
  • 字段类型选对了,后面少改八遍
  • 约束写在数据库层面,业务逻辑用Python
  • 默认值让用户少点一下,就是成功

好了,这一章的内容就到这儿。下一章咱们聊聊视图和界面,那才是用户真正看到的东西。到时候我会分享一些让界面「看起来就很专业」的小技巧。嗯,期待一下。