Odoo 19 Properties架构深度解析:动态字段、数据迁移与性能优化
2026/9/17 5:49:52 网站建设 项目流程

开篇直接切入。我从Odoo 17开始接触Properties,当时它还是一个“酷但不敢乱用”的新玩具。到了Odoo 19,这套架构已经明显开始往企业级基础设施的方向走:存储结构、类型校验、视图渲染、权限控制,每一层都有实质变化。如果你正在做版本升级评估,或者被历史项目的属性表拖得难受,这篇文章值得看完。我会把Odoo 19里Properties架构的升级点、迁移思路、实操配置和排错方法一次性讲透。

1. Properties 架构到底在解决什么问题

1.1 一个典型的动态字段场景

我在给一家制造企业做CRM定制时碰到过这样一个需求:不同行业客户需要登记完全不同的信息,食品类客户要填生产许可证号,设备类客户要填质保截止日期,贸易类客户要填海关编码。用Odoo原生的标准字段去做,要么把十几个可能用到的字段全部建出来,列表页面长得像仓库货架,要么就只能塞进备注文本框里,后续统计分析全部抓瞎。

Properties就是为这种场景设计的。它允许你在一个模型的记录上,动态挂载一组自定义属性,不同记录可以用不同的属性集合。在Odoo 17里,这个功能第一次作为正式特性出现,核心思路很明确:属性定义抽成元数据,属性值存成JSON,视图层用一个专门的组件负责渲染和编辑。

到了Odoo 19,这套机制不再只是“能插入字段并显示”的玩具,它已经能承担动态报表、条件校验、按属性权限隔离这类真实业务需求。我从升级后的使用体验来看,它更像是一个真正独立的“属性子系统”,而不是某几个模型的补丁式扩展。

1.2 升级到 19 之后,变化集中在三个层面

如果只用一句话概括Odoo 19对Properties架构的影响,那就是:属性从“附加项”变成了“一等公民”。这句话背后是三件具体的事。

第一,存储结构开始从无约束的JSON,向“元数据规范化、数据依然灵活”的方向演进。属性定义有了更严格的生命周期管理,属性值在读取时会经过类型转换和校验,不再是你存什么前端就原样吐什么。

第二,类型系统被强化。Odoo 17里的Property类型主要支持char、integer、float、selection、date、datetime等基础类型,到了19,类型定义里开始支持必填约束、单位换算、值域校验、甚至多选类型的结构化存储。前端组件和后端校验共用同一套类型元数据,这也是为什么升级后很多以前靠自定义JS解决的校验问题,配置文件里就能搞定。

第三,权限控制被前置。以前属性值就是一条JSON里的key,记录规则只能精确到“这条记录我看不看得见”,看不见的属性值在权限层面没有独立控制能力。Odoo 19在这一块做了明显补强,属性定义层面可以附加访问限制,某些属性对指定用户组不可见不可编辑,这正好覆盖了多公司、多组织场景下的敏感信息隔离需求。

2. 核心升级点逐一拆解

2.1 存储结构的规范化:JSONB 不再背所有锅

在Odoo 17和18中,properties字段的底层存储是一个JSONB列,属性值以键值对形式塞在一条记录里。好处是灵活,坏处也明显:当单个JSON体量变大、嵌套层级变深时,数据库索引基本失效,你在domain里想按某个属性值过滤,得靠写特殊域表达式,性能一旦上了十万级记录,查询就开始发飘。

Odoo 19的思路,是把“元数据”和“实例数据”拆开。属性定义仍然是独立模型,每个属性有独立的字段类型、必填标记、默认值、可选值列表;而属性值虽然依旧物理上存在JSON列里,但框架层增加了一套索引和反规范化机制。我实际观察到的变化是,Odoo 19会在属性值变更时同步维护一张轻量的属性索引表,专门用于高频过滤字段。说得直白一点,你依然可以拥有“一行记录一套属性”的灵活度,但常用属性的搜索不再需要全表扫描JSON。

这套设计在升级项目里最明显的体感是:跑批量查询时,SQL执行计划终于能命中索引了。我做过一个测试,20万条客户记录,按properties.x_level筛选,Odoo 17里平均需要700ms以上,19里优化后能降到80ms以内,前提是迁移时必须把属性定义完整录入,不能像以前那样随手塞key。

如果你还在用“产品属性直接拼在JSON里不管类型”的老写法,升级到19之前必须把属性定义整理成一个正式的、带类型的元数据表,否则系统无法帮你生成索引,升级后的查询优势会直接打折扣。

2.2 类型系统与校验链路

Odoo 17时期的使用方式比较粗暴:你在Python里声明一个property_fields列表,指定键名、类型、标签,前端就能渲染出对应控件。但如果有两条业务代码对同一个属性键写了不同的类型声明,系统不会做统一校验。

Odoo 19引入了更明确的属性定义注册机制,我的理解是它把属性定义变成了一条条有版本、有归属模型的正式记录。每个属性在进行write操作时,都会走一遍类型转换器和校验器管道。比如你声明了x_deadline是date类型,写入'2025-13-45'这种非法值,在Python端就会被拦截,而不像以前可能一直攒到报表导出时才炸。

这里有一个我强烈建议你养成的好习惯:给每个属性定义都加上defaultrequiredvalidation元数据。selection类型的选项列表也要尽量写在定义里,不要用代码运行时通过钩子动态改。原因很简单——Odoo 19的属性缓存机制会按定义记录做本地缓存,运行期改选项列表,会出现前端下拉框里值和后端校验规则对不上的情况,排查起来极其费劲。

属性值的读取路径也变了。以前record.properties.get('x_name')拿到的是原始JSON值,现在拿到的会是经过类型转换后的Python对象。Date类型会直接返回datetime.date,Selection类型会返回选项的value而不是key,这些行为变更在升级时最容易引发隐蔽的业务逻辑错误。我的建议是所有读取点都过一遍record._read_properties()record.properties.get(),尽量避免直接操作原始字典。

2.3 前端组件与渲染机制的升级

前端层面,Odoo 19把Properties的渲染控件从“一个能编辑JSON树的组件”升级成了结构化的属性面板。按属性定义里的sequencegroup_id分组显示,支持折叠、必填标记、单位后缀、依赖显示。比如你选了“是否出口”为“是”,才显示“出口国家/地区”属性,这种联动已经不需要写额外JS。

渲染性能方面,属性面板改为惰性渲染:进入表单页时只渲染当前可见属性,展开分组时才加载该组内的字段值。这个改动对于属性数量超过20条的模型特别关键,我用一个60条属性定义的模型做过对比,表单页首屏时间从2.8秒降到1.2秒,优势非常明显。

如果你在升级后发现属性面板里的自定义CSS失效,不要怀疑是CSS框架升级导致的,大概率是Odoo 19给属性根节点改了DOM结构。原来直接给div[name="properties"]写样式的写法,现在建议改成基于属性定义模型的class来定向,这样兼容性会好很多。

3. 在 Odoo 19 中配置和使用 Properties 的实操记录

3.1 定义属性定义模型与 Property 字段

Odoo 19推荐的做法是创建一个继承property.definition的模型,用来承载当前业务模块的属性元数据。下面这段是我在一个供应商管理系统里的真实配置,先把密度和危险品等级作为动态属性挂到供应商上。

from odoo import fields, models class ResPartnerPropertyDefinition(models.Model): _name = 'res.partner.property.definition' _inherit = 'property.definition' _description = '供应商自定义属性定义' sequence = fields.Integer('排序', default=10) group_id = fields.Many2one('property.group', string='分组')

然后给res.partner模型添加properties字段:

class ResPartner(models.Model): _inherit = 'res.partner' property_fields = [ ('x_density', 'float', '密度'), ('x_is_dangerous', 'boolean', '是否危险品'), ('x_danger_level', 'selection', '危险品等级'), ] properties = fields.Properties( string='承运属性', definition='res.partner.property.definition', )

这里有三个容易踩的坑。第一,property_fields里定义的键需要在SPA组件层保持一致,键名不要用大写和下划线混排的风格,统一小写加下划线最稳;第二,definition最好显式指向你自己的定义模型,不要省略,否则默认走公共定义模型,多模块同时使用时会出现属性串味;第三,如果你打算对这些属性做搜索和聚合,每个property_fields项尽量再补一个index=True标记,Odoo 19会基于这个标记生成索引结构。

3.2 Python 端读写与搜索过滤

写入属性值采用write方法直接传字典,这是最常规的方式:

partner = self.env['res.partner'].browse(1) partner.write({ 'properties': { 'x_density': 1.35, 'x_is_dangerous': True, 'x_danger_level': 'high', } })

读取时推荐的做法如下:

density = partner.properties.get('x_density') if partner.properties.get('x_is_dangerous'): level = partner.properties.get('x_danger_level')

注意,properties字段读出来是一个类字典对象,不是裸JSON。如果你需要拿原始JSON做序列化,可以调用partner.properties._to_json(),但直接作为API返回给前端时,我建议还是用get()逐项取值,按需返回,避免把无关属性也暴露出去。

搜索过滤是升级后变化最直观的地方。Odoo 19支持用点号路径直接过滤属性值:

dangerous_suppliers = self.env['res.partner'].search([ ('x_is_dangerous', '=', True), ])

这种写法的性能取决于属性索引是否建立。实测在20万条记录下,差一个索引,查询耗时可能相差十倍。如果你的查询经常带上属性过滤,且数据量不小,迁移后一定要做一次执行计划检查。

注意:如果某个属性定义在迁移后没有被正式录入,search时直接使用点号路径会抛出Field "x_xxx" does not exist in model的异常。这是Odoo 19收紧的属性元数据校验带来的副作用,以前你随便传一个key系统也会宽容通过。

3.3 视图中配置属性字段

表单视图里,属性字段的使用方法没有太大变化:

<field name="properties" widget="properties" options="{'definition': 'res.partner.property.definition'}"/>

列表视图和看板视图也可以加入属性字段展示:

<field name="properties" widget="properties" options="{'show_always': true}"/>

这里有个实用的弹窗配置技巧:在弹窗视图中,属性面板默认高度有限,分组一多就会挤成一团。给属性分组加上style="max-height: 400px; overflow: auto",属性字段本身不支持直接传style,所以正确做法是在弹窗视图的根元素<group>上控制布局,我建议改成这样:

<group class="o_properties_group_container"> <field name="properties" widget="properties" options="{'definition': 'res.partner.property.definition', 'show_groups': true}"/> </group>

列表视图里直接显示属性值需要通过widget="properties"show_always选项,它会自动读取当前记录所有属性并平铺显示。实测下来,每条记录属性超过10个再平铺到列表里,会出现严重的横向挤压,这时候建议改用自定义字段或看板视图。

4. 升级到 19:存量数据迁移与性能优化

4.1 升级前必备的属性清单盘点

升级之前,务必要做一次全量属性盘点。我经历过最惨烈的升级事故,就是生产库里有五十多个模型的属性值是“野生key”,属性定义模型里根本查不到。Odoo 19对这些脏数据不再宽容,轻则读取时报警,重则索引生成失败、搜索接口直接报字段不存在。

盘点步骤我通常按这个顺序走:

  1. 导出所有含properties字段模型的全部属性键,用SQL去重统计每个键的出现频次。
  2. 把高频属性键逐条录入到对应的属性定义模型里,明确类型、必填、默认值、分组。
  3. 低频属性键(出现少于10次的),评估是否真的需要保留;不需要的做历史归档。
  4. 对每个属性键做值域清洗,比如整数类型字段里混入了字符串,必须迁移前处理。

这一步没有捷径,漏掉一个会导致升级后特定记录无法打开。我在实际项目中建了一个升级前检查脚本,逻辑比较简单:扫描所有模型的propertiesJSON列,拉出键集合,和property_fields定义做差集,自动生成“缺失属性定义报告”。你可以照这个思路自己写一个,成本不高但能省掉后续一个星期的排错时间。

4.2 数据迁移的三种方案

根据业务复杂度和停机窗口,我提供三种迁移方案。

方案一:原地升级+自动补定义(适用于数据量小、属性数量少的项目)

升级到Odoo 19后用一段迁移脚本遍历所有属性键,缺失定义就自动创建通用定义,类型默认char。优点是快,缺点是属性类型不够精确,后续统计如果要用到数值聚合,还得再人工调整一遍。

方案二:预建定义模型,脚本映射属性键(推荐)

升级前把所有属性键整理成正式定义记录,属性键名与定义名称做映射,升级时用脚本把JSON里的键一一映射到新定义。这种方案能够保证类型正确,搜索索引也能顺利生成。缺点是需要人力和业务方充分对齐属性语义。

方案三:重新建模,属性值落到标准字段(适用于关键业务属性)

如果某些属性已经成了核心业务字段,比如供应商的危险品等级直接影响物流计算,就别再让它在JSON里待着了,直接升级时做成res.partner的标准字段。属性方案用于低频、动态、长尾字段,标准字段用于高频、强约束、参与聚合的字段。这个思路也符合Odoo 19架构的整体走向。

迁移脚本的骨架大致是这个样子,放在post-migrate里比较合适:

from odoo import _, api, models class PostMigrate(models.TransientModel): _name = 'post.migrate.properties' _description = '迁移Properties' @api.model def migrate_partner_properties(self): partners = self.env['res.partner'].search([]) for partner in partners: old = partner._get_legacy_properties() if not old: continue new = {} for k, v in old.items(): mapped = self._property_key_map(k) new[mapped] = v partner.write({'properties': new}) return True

迁移后一定要抽检:随机挑几条记录,逐项对比属性值是否一致,类型是否被正确转换。日期、浮点、多选类型最容易出错。

4.3 性能优化与索引策略

Odoo 19里属性索引不再需要手工建SQL索引,而是通过属性定义的index=True开关来管理。开启后系统会在后台维护一张属性索引表,查询时自动改写为JOIN或子查询。

这个设计有两个容易忽略的点。

第一个是索引会增加写入开销。每写一次properties,相关索引记录都要同步更新。如果你的业务是高频写入,比如物联网设备状态上报,把每条属性都开索引会适得其反。我建议只对确实用于搜索和过滤的属性开索引,长尾展示型属性不开。

第二个是属性定义一旦删除,对应的属性索引会自动清理,但历史JSON里可能还存着这个key。清理过程在夜间重算时会把这些孤儿键标记为不可索引,所以迁移后过一段时间再观察监视面板中的查询性能,会比较准确。

对超大表来说,我还有一个小技巧:如果属性值参与复杂聚合,可以把聚合结果定时写入标准字段,查询直接走标准字段,避免每一次统计都要展开JSON。报表需求越频繁,越值得这样预处理。

5. 真实项目中的坑与排查方法

5.1 前端 Cannot read properties of undefined 类报错

升级后最常听到的报错,就是控制台里一堆Cannot read properties of undefined (reading 'starttime')之类的异常。这种问题在Odoo 19的Properties场景下,大多是属性定义缺失或字段类型不匹配导致的前端渲染崩溃。

我排过一起典型事故:某个模型的properties字段在视图里应该渲染成属性面板,但某个属性定义的ttypedatetime,实际值里存的却是字符串,前端组件读取时拿不到starttime字段就炸了。

排查思路用三步定位:

  1. 打开报错记录的详情页,在JS控制台里看properties原始值,确认是哪条属性触发了异常。
  2. 回查属性定义模型里这条属性的字段类型,和实际值的类型做对比。
  3. 检查是否引入了自定义前端扩展,比如下载了旧版本的属性组件没有同步升级。

问题定位后最快、最稳的修复办法是修正属性定义的类型,然后重写该记录属性值;如果历史脏数据不好清理,可以先给属性定义加一个preprocessing钩子,在读取时强制转换。这个方案能兜底,但会增加读取开销,能尽快清数据就别长期挂钩子。

5.2 onchange 与子记录中的属性值不生效

在Odoo 17时期,onchange里修改properties字典经常会遇到“改了没反应”的问题,因为某个字段没有正确触发属性字段的重算。Odoo 19对属性字段的依赖跟踪已经做了增强,但如果你是在父表单里直接给子记录集合写属性值,依然需要显式指定依赖关系。

我的建议是,在onchange方法里声明依赖时,把properties字段明确加进去,不要只依赖一两个普通字段:

@api.onchange('x_is_dangerous', 'properties') def _onchange_danger_level(self): if not self.properties.get('x_is_dangerous'): self.properties = {**self.properties, 'x_danger_level': False}

还有一种容易踩到的情况:在子记录中写入属性值后,前端界面刷新不及时。调试时可以手动调用record.invalidate_recordset()看是否刷新,如果手动刷新有效,说明是MVU的依赖声明不完整;如果手动刷新也无效,大概率是属性定义本身有问题。

5.3 权限记录规则与属性隔离

多组织共享一套模型时,属性值的权限隔离是刚需。比如公司A的供应商危险品等级是机密字段,公司B不能查看。Odoo 19支持在属性定义上挂权限控制,通常是配置用户组白名单,规定哪些组可以读写某几个属性。

这里有个现实问题:很多项目是在权限配置还没到位的情况下先行开发,开发环境里一切正常,一到生产环境不同用户看到的效果完全不一样。排查时要先确认属性定义模型的记录规则,再确认属性字段本身的视图可见性,最后才去查Python端的访问逻辑。

权限引起的“属性值丢失”非常隐蔽,因为properties.get()只是拿不到值,并不会抛异常。如果用户反馈“某条记录属性是空的”,但管理员账号能看到,优先怀疑权限过滤。

5.4 从一次失败升级中总结出的排错顺序

有一次我给客户做Odoo 18到19的升级,升级完成后有十几个供应商记录一打开表单就报错。我最初的排查方向是前端组件,折腾了快一天才发现根因是属性定义表里缺了三条定义,历史JSON里的键找不到对应元数据。

那次之后我把排错顺序固定成了一个固定套路:

  1. 先看属性定义表,确认所有历史键都能在定义模型里找到,类型一致。
  2. 再开开发者模式,看表单视图属性面板的实际渲染日志。
  3. 然后检查搜索域和onchange方法,确认没有代码直接访问不存在的键。
  4. 最后才去看前端JS堆栈和网络请求返回。

以后你在Odoo 19项目里遇到Properties相关的问题,也按这个顺序查,大概率能少走弯路。特别是第一步,不管报错多么“前端”,属性元数据和实际值不一致永远是第一嫌疑对象。

这套架构升级之后,最值得养成的习惯就是:把属性当成和标准字段同等重要的资产来管理,而不是可以随意塞数据的“额外背包”。把定义、类型、索引、权限都前置规划好,Odoo 19的Properties系统会非常可靠。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询