Populate

Populate 模块是用于 Odoo 数据库的合成数据生成框架。它遵循声明式的 Blueprint 模式:用 XML 或 JSON 描述 要创建什么数据,系统便会大规模生成记录,并支持并行执行、统计分布以及生成目标之间的依赖关系。

典型用例:

  • 性能测试 – 生成数千条记录,对查询、视图和报表进行压力测试。

  • 演示环境 – 随模块开箱即用地提供看起来逼真的数据集。

  • 开发 – 快速填充本地数据库,以便开发需要现有数据的功能。

更多内容

duplicate - 复制记录 是一个更简单的工具,可批量复制 现有 记录。

安装

  1. 在你的数据库中安装 populate Odoo 模块。

  2. (可选)安装 Faker 库以启用 fake.* 生成器:

    $ pip install -r odoo/addons/populate/requirements.txt
    

重要

安装随附蓝图的新模块后,必须 升级 populate 模块,这样才能发现其蓝图并将其加载到数据库中:

$ odoo-bin -d <database> -u populate

CLI 命令

$ odoo-bin populate -d <database> -b <blueprint>
-d <database>, --database <database>

目标数据库(必填)。

-b <blueprint>, --blueprint <blueprint>

蓝图名称或完整 xmlid(必填,除非使用了 --resume)。

--seed <seed>

随机数生成器的种子。若省略,则会随机选择一个种子。提供相同的种子可确保结果可复现(确定性生成)。

--scale <factor>

将蓝图中所有记录数量乘以该系数。默认值:1。

-j <workers>, --jobs <workers>

并行工作进程数。使用 auto 可占用所有可用的 CPU 线程。默认值:1。

--resume [session_id]

恢复被中断的会话。不带参数时,恢复最近一个未完成的会话。指定会话 ID 时,恢复该特定会话。

--profile

为本次运行中的每个可执行 populate 作业保存性能分析器追踪记录。

示例

# Run a blueprint at 10x scale using all CPU cores
$ odoo-bin populate -d mydb -b project.fake_project_demo --scale 10 -j auto

# Run with a fixed seed for reproducibility
$ odoo-bin populate -d mydb -b my_module.my_blueprint --seed 42

# Resume the last interrupted session
$ odoo-bin populate -d mydb --resume

# Resume a specific session by ID
$ odoo-bin populate -d mydb --resume 7

# Save profiler traces for the generated workload
$ odoo-bin populate -d mydb -b project.fake_project_demo --profile

对 populate 运行进行性能分析

使用 --profile 测量 populate 运行的运行时开销。该命令会创建 ir.profile 条目,并将它们归入一个以 populate 会话和蓝图命名的性能分析器会话中。

该选项仅对当前命令调用生效,因此同一个被中断的会话之后可以在启用或不启用性能分析的情况下恢复:

$ odoo-bin populate -d mydb --resume 7 --profile

性能分析在单工作进程和多工作进程模式下均可用。每个可执行作业都会创建自己的分析条目,包括大型作业为并行执行而拆分时创建的子作业。规划器作业仅协调拆分出的子作业,本身不创建或更新记录,因此不会被分析。失败的可执行作业尝试也会创建分析条目,因此其追踪记录仍可用于故障分析。

蓝图

蓝图 是 populate.blueprint 的一条记录,它以声明方式描述要创建哪些数据。蓝图通常随模块的 populate/ 数据文件夹一同发布,并在升级 populate 模块时自动加载。

如果模块的 populate/ 文件夹是有效的 Python 包(包含 __init__.py),则会导入其代码,从而允许该模块注册自定义生成器。

蓝图可以用 XML、JSON 或两者同时定义。如果在同一条记录上同时设置了 definition_xml 和 definition_json,则以 XML 定义为准。

操作块

蓝图定义是一个有序的操作块列表。使用 <create/> 创建记录,使用 <write/> 更新目标记录,使用 <function/> 对目标记录调用方法。字段通过 ORM 的 create 或 write 调用持久化,而值是可以被同一块中后续字段、值和函数参数复用的局部变量。

示例

<create model="res.partner" count="500" id="my_partners">
    <value name="email_domain" eval="'example.com'"/>
    <field name="name" generator="fake.company"/>
    <field name="email"
           eval="name.lower().replace(' ', '-') + '@' + email_domain"/>
    <field name="active" eval="True"/>
</create>

<write model="res.partner" ref="my_partners" batched="True">
    <value name="suffix" eval="'imported by populate'"/>
    <field name="comment" eval="suffix"/>
</write>

<function model="res.partner" name="message_subscribe"
          ref="my_partners" batched="True">
    <value name="current_partner" eval="env.user.partner_id.id"/>
    <arg eval="[current_partner]"/>
</function>
model (必填)

Odoo 模型的技术名称,例如 res.partner。

name (function 必填)

要调用的方法名称。

count (create 必填)

要创建的记录数量。

id

create 块的引用标签。后续块可以使用 ref 定位这些记录。

ref

对于 write 和 function 块:对先前创建的批次的引用(其 id)。

domain

对于 write 和 function 块:用于选择目标记录的 ORM 域。如果既未提供 ref 也未提供 domain,则以该模型的所有记录为目标。create 块不能定义顶层 domain,因为它们创建记录,而不是以现有记录为目标。

scale

True (默认)或 False。表示 --scale 系数是否应用于本块的 count。

parallel

True (默认)或 False。表示此作业是否可以跨并行工作进程拆分。当模型的约束要求顺序写入时,将其设为 False。

batched

仅适用于 write 和 function 块。设为 True 时,生成一组值,并为每个可执行作业执行一次写入或函数调用。默认值为 False,即为每条目标记录生成值并执行一次操作。请参阅 写入作业 和 函数作业。

context

一个 Python 字典字面量,会合并到 create、write 或函数调用的 ORM 上下文中。

重要

操作块按文档顺序执行。通过 ref 引用另一个块的块,只能以蓝图中 更早 定义的块为目标。请先定义主数据,再定义依赖它的记录:

示例

<!-- 1. Stage definitions (master data) -->
<create model="project.task.type" count="8" id="task_types" scale="False">
    <field name="name" generator="fake.bs"/>
</create>

<!-- 2. Projects (reference stages) -->
<create model="project.project" count="120" id="projects">
    <field name="type_ids" ref="task_types" count="8"/>
</create>

<!-- 3. Tasks (reference projects and stages) -->
<create model="project.task" count="10000" id="tasks">
    <field name="project_id" ref="projects"/>
    <field name="stage_id"   ref="task_types"/>
</create>

JSON 格式

JSON 格式与 XML 结构相对应。必需的 operation 键对应于 XML 操作元素名称(create、write 或 function)。顶层数组映射到有序的操作块列表:

示例

[
    {
        "operation": "create",
        "model": "res.partner",
        "count": 500,
        "id": "my_partners",
        "values": {
            "email_domain": { "eval": "'example.com'" }
        },
        "fields": {
            "name":   { "generator": "fake.company", "null_ratio": "0" },
            "email":  { "eval": "name.lower().replace(' ', '-') + '@' + email_domain" },
            "active": { "eval": "True" }
        }
    },
    {
        "operation": "write",
        "model": "res.partner",
        "ref": "my_partners",
        "batched": true,
        "values": {
            "suffix": { "eval": "'imported by populate'" }
        },
        "fields": {
            "comment": { "eval": "suffix" }
        }
    },
    {
        "operation": "function",
        "model": "res.partner",
        "name": "message_subscribe",
        "ref": "my_partners",
        "batched": true,
        "values": {
            "current_partner": { "eval": "env.user.partner_id.id" }
        },
        "args": {
            "0": { "eval": "[current_partner]" }
        }
    }
]

每个对象可选的 fields、values 和 args 键将目标名称映射到其属性字典,即与你在 XML 属性中编写的键相同。位置函数参数使用数字字符串键("0"、"1" 等)。

字段、值与参数

每个 <field/>、<value/> 和 <arg/> 声明都描述一个生成目标:

  • <field/> 在 create 和 write 块中有效,并通过 ORM 持久化。

  • <value/> 在每个操作块中都有效,并定义一个不持久化的局部变量。

  • <arg/> 在 function 块中有效,并定义一个生成的方法参数。带有 name 的参数作为关键字参数传递;未命名的参数按声明顺序按位置传递。

它们支持相同的生成属性,不同之处在于对于按位置传递的 <arg/> 声明,name 是可选的。

示例

<field name="age" generator="scalar.integer" start="18" end="65"
       distribution="normal(mean=35, std=12)"/>
name (field 和 value 必填)

ORM 字段名、本地值名称或关键字参数名称。

generator

要使用的生成器(请参阅 生成器)。与 eval 互斥。对于字段,如果两者都未提供,则会根据字段类型选择一个 默认生成器。值和参数没有可用于选择默认值的 ORM 类型。

eval

一个 Python 表达式。可以按名称引用其他生成目标以生成计算值。与 generator 互斥。

null_ratio

生成 False 而非真实值的概率(0–1)。默认值:0。不能与必填字段或加权 values 组合使用。

unique

True 表示强制唯一性。对于 ORM 字段,生成的值会同时与数据库中已有的记录以及同一作业内先前生成的值进行比较检查。对于本地值和参数,唯一性仅在当前作业内适用,因为它们不会被持久化。

values

显式的值列表或加权字典。例如:"['a', 'b', 'c']" (权重相等)或 "{'a': 3, 'b': 1}" (a 的可能性是 b 的 3 倍)。

distribution

统计分布规范,例如 "normal(mean=50, std=10)"。请参阅 分布。不能与加权 values 组合使用。

domain

用于筛选关联记录的 ORM 域。仅适用于关联生成器和引用生成器。可以包含在生成时解析的生成目标引用,请参阅 动态域。

ref

将关联选取限制为在此引用标签下创建的记录。支持点路径遍历,请参阅 Ref 点路径导航。

comodel_name

对于关联值或参数为必填项,此时无法从 ORM 字段推断 comodel。

partition

True 表示在各并行工作进程间划分 comodel ID。请参阅 并行执行的分区。

默认生成器

当字段既未指定 generator 也未指定 eval 时,会根据字段类型自动选择一个默认生成器:

字段类型

默认生成器

boolean

scalar.boolean

integer

scalar.integer

float

scalar.float

monetary

scalar.monetary

char

textual.char

text

textual.text

html

textual.text

date

temporal.date

datetime

temporal.datetime

selection

choice.selection

binary

binary.binary

many2one

relation.one

one2many

relation.many

many2many

relation.many

many2one_reference

reference.one

reference

reference.raw

properties

properties.value

properties_definition

properties.definition

如果字段类型未在上表中列出,且未提供 generator 或 eval,则会引发错误。

生成器

生成器是生成字段、本地值和函数参数的构建单元。每个生成器都有一个 name (用于在蓝图中引用它)以及一组兼容的目标类型。

标量生成器

生成数值和布尔值。

scalar.boolean

生成 True 或 False。使用 values 可以对概率加权:values="{'True': 9, 'False': 1}" 大约 90% 的情况下生成 True。

兼容目标:boolean 字段以及生成的值或参数。

scalar.integer

在范围内生成随机整数。

兼容目标:integer 和 float 字段,以及生成的值或参数。

start

下界(含)。默认值:1。

end

上界(含)。默认值:1000000。

示例

<field name="quantity" generator="scalar.integer" start="1" end="100"/>
scalar.float

在范围内生成随机浮点数。

兼容目标:float 字段以及生成的值或参数。

start

下界。默认值:1.0。

end

上界。默认值:1000000.0。

scalar.monetary

在范围内生成随机金额值。取决于模型的货币字段,必须在同一蓝图块中为该字段生成(或通过 eval 计算)一个值。

兼容目标:monetary 字段以及生成的值或参数。

start

下界。默认值:1.0。

end

上界。默认值:1000000.0。

文本生成器

生成随机字符串。

textual.char

从字符集中生成固定长度的随机字符串。

兼容目标:char 和 html 字段,以及生成的值或参数。

char_set

可选取的字符。默认值:ASCII 字母和数字。

length

生成字符串的长度。默认值:12。

textual.text

生成固定长度的随机文本块。

兼容目标:text 和 html 字段,以及生成的值或参数。

char_set

可选取的字符。默认值:ASCII 字母、数字、空格和换行符。

length

生成文本的长度。默认值:50。

小技巧

如需看起来逼真的文本(姓名、电子邮件、地址),请改用 fake.* 生成器。

时间生成器

使用相对日期语法,在范围内生成日期和日期时间。

temporal.date

生成随机日期。

兼容目标:date 和 datetime 字段,以及生成的值或参数。

start

范围的开始。默认值:None (时间起点)。

end

范围的结束。默认值:None (时间终点)。

temporal.datetime

生成随机日期时间。

兼容目标:datetime 字段以及生成的值或参数。

start

范围的开始。默认值:None (时间起点)。

end

范围的结束。默认值:None (时间终点)。

两个生成器都接受用于 start 和 end 的 相对日期语法:

  • temporal.date 以 today 为锚点:"today -6m"、"today +1y"

  • temporal.datetime 以 now 为锚点:"now -30d"、"now +2h"

支持的后缀:y (年)、m (月)、w (周)、d (天)、h (小时)、M (分钟)、s (秒)。

示例

<field name="date_order" generator="temporal.date" start="today -6m" end="today"/>
<field name="create_date" generator="temporal.datetime" start="now -30d" end="now"/>

选择生成器

从集合中选取值。

choice.sample

从显式的 values 列表(必填)中选取。支持加权值。

兼容目标:integer、float、char、text、html、date、datetime、boolean 和 selection 字段,以及生成的值或参数。

示例

<field name="priority" generator="choice.sample"
       values="{'high': 1, 'medium': 5, 'low': 4}"/>
choice.selection

从字段自身的选项键中选取。如果提供了 values,则只使用这些键(可带权重)。否则,所有有效的选项键被选中的可能性相同。

兼容类型:selection。

示例

<!-- All selection values equally likely -->
<field name="state" generator="choice.selection"/>

<!-- Only these values, with weights -->
<field name="state" generator="choice.selection"
       values="{'draft': 1, 'confirmed': 5, 'done': 3}"/>

二进制生成器

生成二进制数据。

binary.binary

生成随机二进制数据。

兼容目标:binary 字段以及生成的值或参数。

size

大小(以字节为单位)。默认值:1024。

binary.image

生成随机的纯色图像(PNG)。

兼容目标:binary 字段以及生成的值或参数。

width

图像宽度(以像素为单位)。默认值:64。

height

图像高度(以像素为单位)。默认值:64。

关联生成器

通过从现有记录中选取来生成关联目标。

relation.one

选取单条关联记录。

兼容目标:many2one 字段以及生成的值或参数。

domain

用于筛选候选记录的 ORM 域。请参阅 动态域。

ref

限制为在此引用标签下创建的记录。请参阅 Ref 点路径导航。

comodel_name

对于生成的值或参数为必填项,此时无法从 ORM 字段推断 comodel。

partition

在各并行工作进程间划分 comodel ID。请参阅 并行执行的分区。

relation.many

选取多条关联记录(用于 one2many 和 many2many 字段)。

兼容目标:one2many 和 many2many 字段,以及生成的值或参数。

count

要关联的关联记录的平均数量。

std

数量的标准差。默认值:0 (始终恰好为 count)。

groupby

按 comodel 上的字段对关联记录进行分组。

domain、ref、comodel_name、partition

与 relation.one 相同。

示例

<field name="tag_ids" generator="relation.many" count="3" std="2"/>

动态域

关联生成器上的 domain 参数可以包含 生成目标引用,这些引用会在生成时针对当前记录已生成的值进行解析:

示例

<field name="project_id" generator="relation.one"/>
<field name="task_id" generator="relation.one"
       domain="[('project_id', '=', project_id)]"/>

域表达式中的 project_id 会被自动检测为依赖项。生成时,该表达式会使用为 project_id 生成的实际值求值,因此每个 task_id 都保证属于其同级的 project_id。

Ref 点路径导航

ref 属性支持 点路径遍历,可将选取范围限定到此前创建的批次所 关联 的记录:

示例

<!-- Create projects and their tasks -->
<create model="project.project" count="10" id="my_projects">
    <field name="name" generator="fake.bs"/>
</create>
<create model="project.task" count="100">
    <field name="project_id" generator="relation.one" ref="my_projects"/>
</create>

<!-- Assign timesheets only to tasks that belong to our projects -->
<create model="account.analytic.line" count="200">
    <field name="task_id" generator="relation.one" ref="my_projects.task_ids"/>
</create>

ref="my_projects.task_ids" 的解析过程是:先获取在 my_projects 下创建的记录,再遍历 task_ids 关系,并把选取范围限制在这些 ID 上。任何有效的 ORM 点路径都可使用。

这主要适用于蓝图中未显式创建的伴随记录(corecords),例如随 product.template 自动创建的 product.product 记录。

并行执行的分区

从关联模型(comodel)中选取值的生成器,如 relation.one、relation.many、reference.one、reference.raw,均支持 partition 参数。在并行作业中启用后,关联模型 ID 会通过轮转(round-robin)分区分配到各个工作线程:

示例

<field name="user_id" generator="relation.one" partition="True"/>

这样可避免在并行创建关联记录时发生冲突。

注解

  • 仅当作业存在同级子作业(即已被拆分以并行执行)时,分区才会生效。在单工作线程模式下,该参数不起作用。

  • 与非均匀分布一起使用时,分区可能带来轻微偏差。分布的整体形态会保持不变,但参数不会被那么精确地遵循。大多数情况下可以忽略这一点。

引用生成器

为引用类型字段生成值。

reference.one

为 many2one_reference 字段选取一条记录。它隐式依赖于存储模型名称的那个字段。

兼容类型:many2one_reference。

partition

在各并行工作线程之间分配 ID。

reference.raw

为 reference 字段选取一条记录(存储 "model_name,id" 形式的字符串)。

兼容类型:reference。

res_model

限定为特定模型。

res_id

限定为特定记录 ID。

ref

限定为此引用标签下的记录。

partition

在各并行工作线程之间分配 ID。

Faker 生成器 fake.*

封装了 Faker 库。允许的提供者(provider)中的任何方法都可以直接以 fake.<method_name> 的形式使用:

示例

<field name="name"  generator="fake.name"/>
<field name="email" generator="fake.email" locale="fr_FR"/>
<field name="phone" generator="fake.phone_number"/>
<field name="bio"   generator="fake.paragraph" nb_sentences="5"/>

方法专用的关键字参数(例如 nb_sentences 等)会原样转发给对应的 Faker 方法。

locale

本地化数据所用的区域设置。默认值:en_US。

允许的提供者:address、automotive、bank、barcode、color、company、credit_card、currency、emoji、file、geo、internet、isbn、job、lorem、misc、passport、person、phone_number、profile、sbn、ssn、user_agent。

重要

Faker 需单独安装。请参阅 安装。

其他生成器

misc.counter

生成等差数列。达到 end 后会回绕到 start。

兼容目标:integer 和 float 字段,以及生成的值或参数。

start

初始值。默认值:0。

step

每条记录的增量。默认值:1。

end

上界(到达后回绕)。默认值:None,即不回绕。

示例

<field name="sequence" generator="misc.counter" start="1" step="1"/>
misc.cycle

按顺序、确定性地循环遍历 values 列表。与 choice.sample 不同,它不是随机的,而是精确重复该序列。

兼容目标:integer、float、char、text、html、date 和 datetime 字段,以及生成的取值或参数。

注解

misc.cycle 不允许使用带权重的值,它始终按顺序循环取值。

示例

<field name="day" generator="misc.cycle"
       values="['Mon', 'Tue', 'Wed', 'Thu', 'Fri']"/>
misc.eval

计算一个 Python 表达式。可以引用其他目标名称来产生计算值。

兼容类型:任意类型。

求值上下文包含以下内容:

  • env:Odoo 环境

  • model:正在填充的模型

  • Command:用于构建关系命令的 odoo.fields.Command

示例

<field name="display_name" generator="misc.eval"
       eval="name + ' (' + str(email) + ')'"/>

属性生成器

为 properties / properties_definition 字段体系生成值。

properties.definition

生成属性模式(属性定义列表)。

兼容类型:properties_definition。

props

显式列出属性名称的列表。

count

要生成的属性数量(在未设置 props 时使用)。

allowed_types

将生成的属性类型限定为此集合。

possible_values

对于选择类型的属性:将属性名称映射到其可能值的字典。

properties.prop

用于定义单个属性条目的辅助工具,在 properties.definition 内部使用。

兼容目标:生成的取值或参数。

prop_type

属性类型,例如 char、integer、selection。

string

该属性的显示标签。

possible_values

对于选择类型:可能值的列表。

properties.value

为 properties 字段生成值,并与其父记录的 properties_definition 字段所定义的模式相匹配。

兼容类型:properties。

分布

默认情况下,生成器在其取值范围内均匀随机地产生值。添加 distribution 参数会改变范围内各部分被采样的 概率。

示例

<field name="age"   generator="scalar.integer" start="18" end="90"
       distribution="normal(mean=35, std=12)"/>
<field name="delay" generator="scalar.float"   start="0"  end="100"
       distribution="exponential(rate=0.05)"/>

normal(mean, std):大多数值靠近中心

生成经典的正态钟形曲线。大多数值落在 mean 附近;离它越远,出现得越少。std 即标准差,用于控制离散程度:std 越小,值就越集中在均值附近。

适用场景:当你需要一种贴近现实的“平均值并带有自然波动”的模式时。

示例字段

参数

理由

员工年龄

normal(mean=35, std=12)

大多数员工在 35 岁左右,特别年轻或特别年长的人较少

产品价格

normal(mean=50, std=15)

价格集中在 50 左右,并有少量更便宜或更贵的离群值

任务时长(小时)

normal(mean=8, std=3)

大多数任务耗时约一天,少数更短或更长

uniform():任何值的可能性都相同

平坦分布:范围内的每个值出现的概率完全相同。这是完全省略 distribution 时的默认行为,因此很少需要显式写出。

exponential(rate):大量小值,大值很少

一条从高处开始迅速下降的陡峭曲线。生成的值大多很小;值越大就越罕见。rate 越大,下降得越快。

适用场景:数据应偏向低端,偶尔出现尖峰。

示例字段

参数

理由

距截止日期的天数

exponential(rate=0.03)

大多数截止日期临近,少数在几个月之后

分配的工时

exponential(rate=0.1)

大多数任务很快,少数耗时很长

事件之间的时间间隔

exponential(rate=0.05)

短间隔常见,长间隔罕见

beta(alpha, beta):0 到 1 之间的值,形态可按需要调整

始终产生 [0, 1] 范围内的值。生成器会自动把它映射到你的 start/end 范围上。两个参数决定曲线的形状:

  • alpha=2, beta=2:钟形,以 0.5 为中心(类似有界的正态分布)

  • alpha=1, beta=3:偏向 0(大多数值较低)

  • alpha=3, beta=1:偏向 1(大多数值较高)

  • alpha=0.5, beta=0.5:U 形,值聚集在 0 和 1 附近

适用场景:为百分比、进度、评分或任何有界比例建模。

示例字段

参数

理由

项目进度(%)

beta(alpha=2, beta=2)

大多数项目大致处于中途,少数为 0% 或 100%

折扣率

beta(alpha=1, beta=3)

大多数折扣幅度较小,大额折扣很少

满意度评分

beta(alpha=3, beta=1)

大多数评分较高

poisson(lam):某件事发生的次数

产生表示 出现次数 的整数。lam 即 lambda,是你预期出现的平均次数。接近 lam 的值最可能出现,离它越远越罕见。

适用场景:需要生成“多少”时,例如条目数、事件数或尝试次数。

示例字段

参数

理由

订单行数

poisson(lam=5)

订单平均有 5 行,有些只有 1 行,很少达到 15 行以上

每天的支持工单数

poisson(lam=3)

平均每天约 3 个

登录尝试次数

poisson(lam=2)

通常为 1–3 次,偶尔更多

triangular(min, max, mode):三点估算

简单的三角形分布。mode 是峰值(最可能的值),min 和 max 是绝对边界。mode 附近的值最常见,概率向两端线性递减。

适用场景:能估算出三个点(最小值、最大值和最可能值),但没有更详细的数据时。

示例字段

参数

理由

任务估算(天)

triangular(min=1, max=30, mode=5)

大多数任务约需 5 天,不会少于 1 天,也不会超过 30 天

运费

triangular(min=5, max=200, mode=25)

通常在 25 左右,限制在 5 到 200 之间

快速决策指南

你的需求…

使用

围绕平均值形成真实聚集

normal

所有值的可能性相同

uniform,或省略 distribution

大多是小值,大值罕见

exponential

百分比 / 有界比例

beta

“多少次”的计数

poisson

三点估算(最小值 / 最可能值 / 最大值)

triangular

高级主题

生成值

字段是传给 ORM create 或 write 的持久化值。相比之下,<value/> 声明是生成的局部变量,但 不会持久化。借助它们可以构建多个字段共同依赖的中间值,从而避免重复:

示例

<create model="account.move.line" count="1000">
    <field name="quantity"    generator="scalar.integer" start="1" end="100"/>
    <field name="price_unit"  generator="scalar.float"   start="5" end="500"/>
    <value name="subtotal" eval="quantity * price_unit"/>
    <field name="discount" eval="subtotal * 0.1 if subtotal > 200 else 0"/>
    <field name="price_total" eval="subtotal - discount"/>
</create>

这里的 subtotal 会被计算出来,但从不写入数据库。discount 和 price_total 都引用它,因此 quantity * price_unit 的逻辑只存在于一处。

生成值还可用于 关联 持久化字段:

示例

<create model="res.partner" count="200">
    <value name="first_name" generator="fake.first_name"/>
    <value name="last_name"  generator="fake.last_name"/>
    <field name="name"  eval="first_name + ' ' + last_name"/>
    <field name="email"
           eval="first_name.lower() + '.' + last_name.lower() + '@example.com'"/>
</create>

每条记录的 name 与 email 相互保持一致,且任何中间值都不会被单独存储。

写入作业

使用 <write/> 更新已有记录。目标记录可通过 ref、顶层 domain 或两者同时来选择。

示例

<!-- Create partners and tag them with the "customers" reference -->
<create model="res.partner" count="500" id="customers">
    <field name="name" generator="fake.company"/>
    <field name="active" values="{'True': 9, 'False': 1}"/>
    <field name="customer_rank" start="0" end="5"/>
</create>

<!-- Update all partners created under the "customers" reference -->
<write model="res.partner" ref="customers">
    <field name="phone" generator="fake.phone_number"/>
</write>

<!-- Update all active customers, even if they were not created by this blueprint -->
<write model="res.partner"
       domain="[('customer_rank', '&gt;', 0), ('active', '=', True)]">
    <field name="mobile" generator="fake.phone_number"/>
</write>

<!-- Update only active customers created under the "customers" reference -->
<write model="res.partner" ref="customers"
       domain="[('customer_rank', '&gt;', 0), ('active', '=', True)]">
    <field name="email" generator="fake.company_email"/>
</write>

定位规则如下:

属性

目标记录

仅 ref

在该 populate 引用下创建的记录。

仅 domain

作业模型中匹配该域的已有记录。

ref 与 domain

同时匹配该域的引用记录。

均不指定

作业模型的所有已有记录。

write 作业中的域只会求值一次以选择目标记录,不会按每条生成的记录动态变化。create 作业不能定义顶层 domain,因为它们创建新记录,而不是定位已有记录。

默认情况下,每条目标记录都会生成并写入一次值。使用 batched="True" 时,会为整个可执行作业或子作业生成一组值,并通过一次 ORM write 调用更新该记录集:

示例

<write model="res.partner" ref="customers" batched="True">
    <field name="active" eval="True"/>
</write>

batched 仅对 write 和 function 块有效。create 块不接受该属性,因为 ORM create 已经接收生成值的列表。大型作业仍可拆分为子作业,因此每个子作业执行自己的写入。

函数作业

使用 <function/> 对目标记录调用模型方法。当记录无法直接以其最终业务状态创建,而必须经过过渡方法时,这很有用。例如,发票先创建为草稿,然后可通过调用 _post 过账:

示例

<function model="account.move" name="_post" ref="moves" batched="True">
    <arg name="soft" eval="False"/>
</function>

function 块遵循与 write 块相同的 ref 和 domain 定位规则。name 属性用于选择方法。使用 @api.model 装饰的方法会在空模型记录集上每个作业调用一次;常规记录方法则在目标记录上调用。

使用 <arg/> 声明方法参数。参数支持与 <value/> 相同的生成属性。命名参数会成为关键字参数,而未命名参数按声明顺序成为位置参数:

示例

<function model="x.model" name="action" ref="records">
    <arg eval="'first positional'"/>
    <arg eval="42"/>
    <arg name="flag" eval="True"/>
</function>

在 JSON 中,位置参数使用 "0" 和 "1" 之类的数字字符串键。默认 batched="False" 时,会生成参数并对每条目标记录调用一次方法。batched="True" 时,会生成一组参数,并对每个可执行作业或子作业的目标记录集调用一次方法。这种批处理差异适用于常规记录方法;@api.model 方法始终每个作业调用一次。

蓝图继承

蓝图支持通过 inherit_id 实现 Odoo 风格的视图继承。子蓝图会对其父级的 XML 定义应用 XPath 或位置规格:

示例

<record id="custom_blueprint" model="populate.blueprint">
    <field name="name">Custom Blueprint</field>
    <field name="inherit_id" ref="base_module.parent_blueprint"/>
    <field name="definition_xml" type="xml">
        <!-- Change record count -->
        <create model="res.partner" position="attributes">
            <attribute name="count">2000</attribute>
        </create>
        <!-- Add a new field to an existing create block -->
        <create model="res.partner" position="inside">
            <field name="website" generator="fake.url"/>
        </create>
        <!-- Add a new create block after an existing one -->
        <create model="res.partner" position="after">
            <create model="res.users" count="50" id="new_users">
                <field name="name" generator="fake.name"/>
                <field name="login" generator="fake.user_name" unique="True"/>
            </create>
        </create>
    </field>
</record>

支持的位置:attributes、inside、before、after、replace。XPath 表达式(<xpath expr="..." position="...">)同样可用。支持链式继承(孙级蓝图);循环继承会被检测并拒绝。

导入蓝图

蓝图继承 扩展的是单个蓝图,而 <import/> 会将多个独立的 XML 蓝图组合成一个新场景。导入的操作块会插入到导入出现的位置,并成为调用方会话的普通作业:

示例

<create model="sale.order" count="500" id="orders">
    <!-- ... -->
</create>

<import ref="product.fake_product_demo" as="catalog"/>

<create model="sale.order.line" count="2000">
    <field name="order_id" ref="orders"/>
    <field name="product_id"
           ref="catalog/product_templates.product_variant_ids"/>
</create>

ref 属性必须包含另一个 populate.blueprint 记录的完整外部 ID。<import/> 必须是蓝图 <data/> 根节点的直接子节点。

可选的 as 属性会为被导入蓝图声明的每个 id 及其内部引用添加命名空间。当场景导入多个具有相同 ID 的蓝图,或多次导入同一蓝图时,这可避免冲突。若不使用 as,ID 保持不变,且不得与调用方或其他导入中的 ID 冲突。

斜杠(/)分隔命名空间各组成部分,而点(.)保留其现有含义,即 遍历关系字段。例如,在 catalog/product_templates.product_variant_ids 中,catalog/product_templates 是 populate 引用,product_variant_ids 是关系字段路径。

无需修改源蓝图即可自定义导入:将 inherit_id 支持的相同继承规格添加为 <import/> 的子节点:

示例

<import ref="product.fake_product_demo" as="catalog">
    <xpath expr="//create[@id='product_templates']" position="attributes">
        <attribute name="count">250</attribute>
    </xpath>
    <xpath expr="//create[@id='product_supplier_info']" position="replace"/>
</import>

XPath 表达式针对的是源蓝图的原始 ID,因为规格在应用命名空间之前就已生效。而应用于组合后蓝图的继承看到的是带命名空间的 ID,例如 catalog/product_templates。

导入会递归解析,因此其命名空间会叠加。例如,在 sales 命名空间下导入 catalog 命名空间会产生 sales/catalog/products 之类的引用。导入与继承的递归组合会被拒绝。

重要

只有 XML 蓝图支持导入;纯 JSON 蓝图不能被导入。调用方的模块必须依赖提供被导入蓝图的模块。当两者位于同一模块时,被导入的蓝图必须在该模块的 populate 数据文件中更早定义。

导入在创建会话时解析。新会话使用最新的蓝图定义,而现有会话保留根据其创建时可用定义实例化的作业。

会话与恢复

每次运行都会创建一个 会话 (populate.session),用于跟踪每个作业及其生成的记录。如果执行被中断(Ctrl+C、崩溃等),可以从上次中断处恢复:

示例

# Resume the most recent unfinished session
$ odoo-bin populate -d mydb --resume

# Resume a specific session by ID
$ odoo-bin populate -d mydb --resume 42

会话还保证 确定性生成:使用相同蓝图提供相同的 --seed,每次都会产生相同的数据。

并行执行

传入 -j 选项(或 -j auto)可将大型作业拆分到多个工作进程。每个超过内部批大小的作业会自动划分为子作业并分发到进程池。

示例

$ odoo-bin populate -d mydb -b my_blueprint --scale 50 -j auto

当模型的约束要求顺序写入时,可通过 parallel="False" 对每个操作块禁用并行。多进程后端由环境变量 ODOO_POPULATE_MULTIPROCESS_ENABLE 控制(默认为 True)。

约束违反时自动重试

会话执行器包含针对暂时性数据库约束失败的重试机制。当作业触发以下 PostgreSQL 违反之一时,会重新生成该作业的种子,并用一组全新的随机值重新执行整个作业(最多 5 次尝试):

违反类型

常见原因

提示

UniqueViolation

两条生成的记录在唯一索引上冲突

使用能产生更多样值的生成器,或添加 unique="True"

NotNullViolation

必需列收到了 NULL

在出问题的字段上添加 null_ratio="0"

CheckViolation

生成的值未通过 CHECK 约束

调整生成器参数以符合约束

ExclusionViolation

生成的值违反了排除约束

调整生成器参数以符合约束

这意味着蓝图无需事先完美调优——因随机性偶尔出现的约束失败会被透明处理。只有所有重试尝试后仍然存在的违反才会作为错误暴露出来。

编写自定义生成器

可以通过继承 odoo.addons.populate.generators.Generator 创建自定义生成器,并将代码放在模块的 populate/ 包中(包含 __init__.py)。模块加载时,生成器会自动注册。

示例

from odoo.addons.populate.generators import Generator


class SequentialEmail(Generator):
    """Generates email addresses like user_0001@example.com, user_0002@example.com, ..."""

    name = 'my_module.sequential_email'
    allowed_on = ('char', 'value')

    def __init__(self, domain_name='example.com', **kwargs):
        super().__init__(**kwargs)
        self.domain_name = domain_name
        self._counter = 0

    def _next(self, known_vals):
        self._counter += 1
        return f'user_{self._counter:04d}@{self.domain_name}'

    @classmethod
    def convert_to_kwargs(cls, attrs):
        kwargs = super().convert_to_kwargs(attrs)
        if 'domain_name' in attrs:
            kwargs['domain_name'] = attrs['domain_name']
        return kwargs

关键要求:

name (类属性,必需)

生成器的唯一字符串标识符。约定:<module_name>.<generator_name>。

allowed_on (类属性,可选)

兼容的 字段类型 元组。包含 value 可让生成器用于生成值和参数。设为 None 可允许任意目标。

_next(self, known_vals) (方法,必需)

生成并返回下一个值。known_vals 是一个字典,将目标名称映射到当前记录中已生成的值(只有 depends 中列出的目标才保证存在)。

convert_to_kwargs(cls, attrs) (类方法,可选)

重写此方法可将 XML/JSON 属性转换为 __init__ 关键字参数。请始终先调用 super().convert_to_kwargs(attrs) 以处理标准属性(values、null_ratio、distribution、unique)。

注册后,该生成器可在任意蓝图中使用:

示例

<field name="email" generator="my_module.sequential_email"
       domain_name="mycompany.com"/>

指导原则

以下指导原则并非硬性规则,但有助于你编写蓝图并处理一些边界情况。

选择能在各层级间清晰伸缩的数量

蓝图的 count 值应在 --scale 1 时产生有用、可浏览的数据集,并在更高缩放级别下保持连贯。一种实用的做法是设定三个层级:

  • 1x(基础)——独立的演示或开发数据集。足够大以检验分页、搜索和筛选,但又足够小,可在数秒内填充完毕。主事务模型约 10 000 条记录是合理的基线。

  • 10x——负载测试规模(约 100 000 条记录)。可暴露界面变慢和未建索引查询的瓶颈。

  • 100x——压力测试规模(约 1 000 000 条记录)。可暴露 ORM 或 PostgreSQL 的可扩展性上限。

示例

$ odoo-bin populate -d mydb -b my_module.demo              # 1x  — 10 000 tasks
$ odoo-bin populate -d mydb -b my_module.demo --scale 10   # 10x — 100 000 tasks
$ odoo-bin populate -d mydb -b my_module.demo --scale 100  # 100x — 1 000 000 tasks

保持比例贴近现实。 绝对数量不如相关模型之间的比例重要。如果你创建 120 个项目和 10 000 个任务,平均每个项目约 80 个任务,这是合理的均值。在 100x 时,会变成 12 000 个项目和 1 000 000 个任务,比例保持不变。

主数据忽略缩放。 阶段定义、属性集及类似配置记录应始终使用 scale="False",以便在所有层级中保持固定数量。1x 时的八个任务阶段在 100x 时仍是八个任务阶段。

使用 context 禁用副作用

禁用邮件通知、字段跟踪和自动创建记录后,批量填充会明显加快。

示例

<create model="account.move" count="15000" id="invoices"
        context="{'mail_auto_subscribe_no_notify': True}">
    ...
</create>

使用 partition="True" 避免多工作进程模式下的序列化错误

并行创建子记录(例如为订单创建订单行)时,只要父模型具有 依赖于子记录的存储计算字段,就应在父字段上添加 partition="True"。

示例

<create model="sale.order.line" count="20000" id="order_lines">
    <field name="order_id" ref="sale_orders" partition="True"/>
</create>

不进行分区时,工作进程会随机选取父 ID。两个工作进程可能同时为同一订单创建订单行。由于 sale.order 具有在 order_line 变化时重新计算的存储计算字段,两个工作进程会同时尝试写入同一订单行。PostgreSQL 会检测到该冲突并抛出 序列化错误。

使用 partition="True" 时,每个工作进程会分配到互不重叠的父 ID 子集。任意两个工作进程都不会接触同一父记录,因此并发写入不会冲突,序列化错误也就不会发生。

使用生成值处理中间逻辑

生成值从不会写入数据库,但能让蓝图更清晰、更易维护。可用于:

关联字段——生成一次值,在多个持久化字段中复用:

示例

<value name="first_name" generator="fake.first_name"/>
<value name="last_name"  generator="fake.last_name"/>
<field name="name" eval="first_name + ' ' + last_name"/>
<field name="email"
       eval="first_name.lower() + '.' + last_name.lower() + '@example.com'"/>

多字段唯一性——将多个字段打包进元组并标记为唯一,然后再解包:

示例

<value name="generated_product_id" generator="relation.one"
       comodel_name="product.product" ref="products"/>
<value name="generated_partner_id" generator="relation.one"
       comodel_name="res.partner" ref="customers"/>
<value name="unique_pair" eval="(generated_product_id, generated_partner_id)" unique="True"/>
<field name="product_id" eval="unique_pair[0]"/>
<field name="partner_id" eval="unique_pair[1]"/>

当两个字段上存在复合唯一约束,而仅对其中一个字段添加 unique=True 会过度限制可能的组合时,这很有必要。

计算数量——先推导出比例,再应用它:

示例

<value name="ratio" generator="scalar.float"
       start="0" end="1" distribution="beta(alpha=2, beta=2)"/>
<field name="qty_delivered" eval="product_uom_qty * ratio"/>

使用 eval 从父记录派生值

当子记录需要一个与其父记录匹配的值(例如子任务继承父任务的项目)时,可使用 eval 搭配 model.browse() 或 env[...]:

示例

<!-- Subtasks inherit the project from their parent task -->
<field name="parent_id" ref="parent_tasks"/>
<field name="project_id" eval="model.browse(parent_id).project_id.id"/>

<!-- Invoice currency matches the journal's currency -->
<field name="currency_id"
       eval="(journal := env['account.journal'].browse(journal_id)).currency_id.id
             or journal.company_id.currency_id.id"/>

使用写入块实现两阶段创建

某些模型要求按特定顺序设置字段,或需要第二次处理来模拟真实的状态转换。可使用 <write/> 更新先前创建的记录:

示例

<!-- Phase 1: create product templates without variants -->
<create model="product.template" count="5000" id="templates"
        context="{'create_product_product': False}">
    <field name="name" generator="fake.catch_phrase"/>
</create>

<!-- Phase 2: add attribute lines (triggers variant creation) -->
<create model="product.template.attribute.line" count="8000" id="attr_lines">
    <field name="product_tmpl_id" ref="templates"/>
    ...
</create>

<!-- Phase 3: update the generated variants -->
<write model="product.product" ref="templates.product_variant_ids">
    <field name="default_code" generator="fake.ean13" unique="True"/>
</write>