# 动态领域模型
本系统基于 JDBC 自研的 ORM 框架,支持在运行时动态定义领域模型(动态模型,类型为 DYNAMIC):定义保存后,系统会自动建表、加列,并为模型提供与 GORM 领域模型一致的增删改查、导入导出和权限控制能力。
# 目标读者
本文档的目标读者为:本系统的实施和开发人员
以下描述了领域模型支持相关功能的一些开发说明,当前包括
- 动态模型的创建方式及命名规则
- 领域模型字段属性设置
- 领域模型扩展元数据配置
- 索引与模型扩展
# 查看领域模型
系统中所有的领域模型(包括平台内置的 GORM 模型和动态模型)都在 开发定制 > 领域模型 中列出,可以按简略名称、完整名称、数据库表名搜索,并通过 详情、修改 查看或修改模型定义及其字段:

当前版本在界面上不能新建模型
开发定制 > 领域模型 列表上没有 创建 按钮:种子数据中 DomainClass 的创建权限要求(createRoleRequirement)为空,而创建权限要求为空时任何用户都没有创建权限(接口 GET /permissions/DomainClass/create 返回 {"create": false})。
因此新建动态模型需要通过 种子数据 CSV 导入,或随动态插件的种子数据一起导入。已有模型的字段可以在 修改 中调整。
修改弹窗中的界面问题
当前版本打开动态模型的 修改 弹窗时,「模型字段」子表每一行的「可选值」列都显示 Unsupported displayType: tag_list,每行末尾多出一个「0」,分组标题「Basic Information」和字段标签「Class Type」也没有翻译。这些是当前版本的界面问题,不影响子表中其他列的显示;字段的可选值可以通过种子数据 CSV 的 options 列设置。
# 通过种子数据创建动态模型
动态模型的定义由两个 CSV 文件导入,文件放在种子数据目录中,文件名以领域模型名称开头(例如 DomainClass_example.csv、DomainClassField_example.csv),导入规则见 数据导入。
模型定义(DomainClass):只需要填写简略名称,其余名称由系统自动生成。
shortName(*),extInfo,createRoleRequirement.name,readRoleRequirement.name,updateRoleRequirement.name,deleteRoleRequirement.name
SampleDynamicOrderDomain,,DEVELOPER,DEVELOPER,DEVELOPER,DEVELOPER
2
后四列是模型级的增、查、改、删权限要求(RoleRequirement 名称)。某项权限要求为空时,任何用户都没有该项权限,因此需要在界面上使用的模型,应把四项都配上。
字段定义(DomainClassField):
domainClass.shortName(*),name(*),dataType,referenceDomain.shortName,nullable,editable,defaultValue,options,extInfo
SampleDynamicOrderDomain,orderId,STRING,,Y,Y,,,
SampleDynamicOrderDomain,isActive,BOOLEAN,,N,N,,,
SampleDynamicOrderDomain,totalAmount,BIG_DECIMAL,,Y,N,,,"{""precision"": 18, ""scale"": 2}"
SampleDynamicOrderDomain,quantity,INTEGER,,N,Y,,,
SampleDynamicOrderDomain,productId,LONG,,Y,Y,,"[1,2,3]",
SampleDynamicOrderDomain,orderDate,LOCAL_DATE,,Y,Y,,,
SampleDynamicOrderDomain,buyerTask,DOMAIN_OBJECT,SampleDynamicOrderDomain,Y,Y,,,
SampleDynamicOrderDomain,sellerTasks,DOMAIN_OBJECT_LIST,SampleDynamicOrderDomain,Y,Y,,,
2
3
4
5
6
7
8
9
# 命名规则及自动生成的名称
模型的简略名称(shortName)和字段的名称(name)都必须匹配正则 ^[a-zA-Z][a-zA-Z0-9]*$:以字母开头,只能包含字母和数字,不能包含下划线、空格或中文。不符合时,创建模型会报 InvalidDomainName(错误码 14002),创建字段会报 InvalidDomainFieldName(错误码 14003)。
创建模型时,系统根据简略名称自动生成以下信息,这些信息创建后不可修改:
| 信息 | 生成规则 | 示例(简略名称 SampleDynamicOrderDomain) |
|---|---|---|
| 完整名称(fullName) | DYNAMIC-<简略名称> | DYNAMIC-SampleDynamicOrderDomain |
| 数据库表名(tableName) | <租户>_<简略名称转为小写下划线形式> | muyan_sample_dynamic_order_domain |
| 显示名称(label) | 按驼峰拆成以空格分隔的单词 | Sample Dynamic Order Domain |

字段的数据库列名由字段名称转为小写下划线形式(例如 orderDate → order_date);如果结果是 PostgreSQL 关键字(如 user、order、group),会加 _col 后缀;列名最长 63 个字符。
# 领域模型字段属性设置
# 字段属性设置
| 字段名 | 描述 |
|---|---|
| 名称(name) | 字段的名称,用于在代码中引用该字段。在同一领域模型中,字段名称不能重复。创建后不可修改 |
| 数据类型(dataType) | 字段的数据类型,详细类型见下文。创建后不可修改 |
| 字段关联类型(referenceDomain) | 数据类型为 DOMAIN_OBJECT 或 DOMAIN_OBJECT_LIST 时必填,指定关联的领域模型;FILE / FILE_LIST 类型会自动关联到 StorageFieldValue。创建后不可修改 |
| 可空(nullable) | 该字段是否可为空,对应数据库列的 NOT NULL 约束 |
| 可修改(editable) | 对象创建后是否可以修改该字段的值 |
| 默认值(defaultValue) | 字段的默认值,对应数据库列的 DEFAULT |
| 可选值(options) | 该字段的可选值列表,为 JSON 数组字符串,格式如 ["option1", "option2"]。设置后会在数据库上建立名为 <列名>_options 的 CHECK 约束,只允许保存这些值 |
| 扩展信息(extInfo) | 其他额外配置信息,不同数据类型可能有不同扩展信息,见下文 |
| 备注(comment) | 字段备注,会写入数据库的列注释(COMMENT ON COLUMN) |
# 数据类型详细说明
# 字段数据类型
系统支持的 字段数据类型(FieldDataType 枚举)包括:
| 数据类型 | 描述 |
|---|---|
| STRING | 字符串 |
| BOOLEAN | 布尔值(true 或 false) |
| BIG_DECIMAL | 高精度十进制数,可在 extInfo 中设置 precision 和 scale |
| INTEGER | 整数 |
| LONG | 长整数 |
| DOUBLE | 双精度浮点数 |
| LOCAL_DATE | 不带时间和时区信息的日期 |
| ZONED_DATETIME | 带时区信息的日期和时间 |
| OFFSET_DATETIME | 带时区偏移量的日期和时间 |
| JSON_STRING | JSON 字符串 |
| DOMAIN_OBJECT | 对另一个领域对象的引用(多对一),需要设置字段关联类型 |
| DOMAIN_OBJECT_LIST | 领域对象列表(多对多),需要设置字段关联类型 |
| ENUM | 枚举,需要在 extInfo 中通过 enumClass 指定 Java 枚举类 |
| ENUM_LIST | 多选枚举,以 jsonb 保存,同样需要在 extInfo 中设置 enumClass |
| FILE | 单个附件 |
| FILE_LIST | 多个附件 |
| MAPPED_DOMAIN_OBJECT | 一对一反向关联字段,例如 A 类中有一个字段 b 为 B 类的对象,则可以在 B 中声明一个 MAPPED_DOMAIN_OBJECT 类型字段 a,referenceDomain 为 A,同时在 extInfo 的 mappedBy 中填写 b,表示反向引用关联到 A 类的 b 字段。这是虚拟字段,不在表中建列 |
| MAPPED_DOMAIN_OBJECT_COLLECTION | 与 MAPPED_DOMAIN_OBJECT 类似,不过为一对多的反向关联 |
| GENERIC_OBJECT | 对任意类型领域对象的引用,以 <简略名称>:<id> 的形式保存 |
| GENERIC_OBJECTS | 对任意类型领域对象的引用列表,以 jsonb 保存 |
# 扩展信息(Ext Info) 字段
不同数据类型的字段可以在扩展信息中配置不同的内容,配置格式为 JSON:
{
// 适用于 Decimal 类型的字段,设置小数位数为 2
// Applicable to Decimal type fields, set the number of decimal places to 2
"scale": 2,
// 适用于 Decimal 类型的字段,设置精度为 10
// Applicable to Decimal type fields, set the precision to 10
"precision": 10,
// 指定 ENUM / ENUM_LIST 类型的字段的 Java 枚举类型定义
// Specify the Java enum type definition of the ENUM / ENUM_LIST type field
// 该枚举类的定义可以放在动态插件中
// The enum class definition can be in a dynamic plugin
"enumClass": "tech.muyan.mes.enums.WorkTaskStatusEnum",
// 对于 MAPPED_DOMAIN_OBJECT/MAPPED_DOMAIN_OBJECT_COLLECTION 类型的字段,必须要设置其反向引用字段
// For MAPPED_DOMAIN_OBJECT/MAPPED_DOMAIN_OBJECT_COLLECTION type fields, set its reverse reference field
"mappedBy": "b"
} 2
3
4
5
6
7
8
9
10
11
12
13
14
15
BIG_DECIMAL:scale表示小数位数,precision表示精度(整数位数 + 小数位数)。两项都设置且都不为 0 时才会建成decimal(precision, scale)列;只设置其中一项,或scale设为0时,按不限精度的decimal建列(因此需要整数时请改用INTEGER或LONG)。ENUM/ENUM_LIST:enumClass为 Java 枚举类的完整类名,该类可以放在动态插件中。MAPPED_DOMAIN_OBJECT/MAPPED_DOMAIN_OBJECT_COLLECTION:mappedBy为反向引用的字段名,必须设置。
# 领域模型元数据
领域模型扩展元数据在模型的 extInfo 中配置:
labelField:在前端的对象控件上,显示对象的哪个属性作为标识;不设置时显示id。inlineSearchColumns:在前端的对象输入控件中快捷搜索对象时,搜索哪些属性。loadAfter:导入种子数据时,该类型的对象需要在哪些类型的对象之后导入。具体描述请参考 导入顺序。queryField:CSV 导入时,关联到该模型的列如果没有在列名中指定查询字段,就用该字段查找关联对象。objectCloneLogicName:复制该模型对象时使用的动态逻辑(逻辑类型OBJECT_CLONE_CORE_LOGIC)名称,不设置时使用系统内置的Default Clone Object Core Logic。
样例如下:
{
// 用户快捷搜索时,使用 name 和 label 两个字段进行搜索匹配
// When users perform quick search, use the name and label fields for search matching
"inlineSearchColumns": ["name", "label"],
// 界面上显示 Object 控件时,显示其 label 字段的值作为标识
// When displaying Object controls on the interface, show the value of its label field as identifier
"labelField": "label",
// 导入 CSV 数据时,当前 Domain 会在 DynamicLogic 和 User 之后加载
// When importing CSV data, the current Domain will be loaded after DynamicLogic and User
"loadAfter": ["DynamicLogic", "User"],
// CSV 中关联到当前 Domain 的列未指定查询字段时,按 name 字段查找
// When a CSV column referencing this Domain has no query field, look up by the name field
"queryField": "name"
} 2
3
4
5
6
7
8
9
10
11
12
13
提示
当前版本的 extInfo 还能解析 dynamicTemplate 和 dynamicEntityInstance 两个配置(自定义实体模板),但平台中没有功能读取它们,配置后不会生效。
# 索引
动态模型的索引通过内置的动态模型 DomainClassIndex 定义,其字段如下:
| 字段 | 说明 |
|---|---|
name | 索引名称 |
domainClass | 索引所属的领域模型 |
fields | 参与索引的字段名称,JSON 数组,例如 ["orderId", "orderDate"] |
unique | 是否唯一 |
创建 DomainClassIndex 对象时,系统会在模型的数据表上建立名为 <表名>_<索引名称小写> 的索引;unique 为 true 时建立同名的 UNIQUE 约束。fields 为空会报 IndexFieldsCouldNotBeEmpty(错误码 14004),字段名称在模型中不存在也会报错。
菜单中没有 DomainClassIndex 的入口,需要通过种子数据或接口创建,需要 DEVELOPER 权限。
提示
删除 DomainClassIndex 对象不会删除数据库中已建立的索引或约束。
# 模型扩展
一个动态模型可以通过 ExtendedDomainClass(字段 parent、child)声明扩展另一个模型:子模型会继承父模型的全部字段,与自身字段合并使用。
ExtendedDomainClass 没有菜单入口,四项权限要求也都为空,界面和接口都不能创建,只能通过种子数据(文件名以 ExtendedDomainClass 开头的 CSV)导入,系统会在导入 DomainClass、DomainClassField 之后导入它。
约束如下:
- 父子模型中同名字段的数据类型必须相同,否则报错;
- 不允许循环扩展(A 扩展 B,B 又扩展 A);
- 父模型中如果有
MAPPED_DOMAIN_OBJECT、MAPPED_DOMAIN_OBJECT_COLLECTION类型(虚拟字段)的字段,而子模型中没有同名同类型的字段,则不能扩展该父模型。
# 集合字段在接口中的输出
动态模型的列表和详情接口会输出全部字段,包括 DOMAIN_OBJECT_LIST 等集合字段。关联对象按层级渲染,最深一层只输出关联对象的 id 和 labelField 字段,不会无限递归。
# Domain 设计相关约定
# extInfo 约定
使用 extInfo 字段来存储结构比较复杂,异构、或者与 Domain 的特定子类型相关,但又需要进行结构化存储的信息。
该字段的相关定义样例如下
// 如下是在平台中实际实现的 extInfo 字段的样例
// Example of the extInfo field actually implemented in the platform
class DynamicFormField implements MultiTenant<DynamicFormField>,
Auditable, Serializable {
// ....
// Java 的字段类型为 String
// The Java field type is String
String extInfo
// 设置为可以为 null, 但是不能为空字符
// Set to be nullable, but not blank
static constraints = {
extInfo nullable: true, blank: false
}
static mapping = {
// 使用 jsonb 类型的数据库字段,数据库会自动增加相关校验,并提供 JSON 查询相关功能
// 在转换为 Java 对象时,会被转换为 String 类型
// Use jsonb field, the database will automatically add relevant checks and provide JSON query related functions
// When converted to a Java object, it will be converted to a String type
extInfo type: 'tech.muyan.rdbms.postgres.JSONBType', sqlType: "jsonb", defaultValue: "'{ }'"
}
// ....
} 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# displaySequence 约定
通常使用 displaySequence 字段来指定字段、字段组、树节点、DynamicAction 等在前端的显示顺序
# name / label / description 约定
Domain 字段常设计不可更新的 name 字段和可更新的 label 字段。
- 通常使用
name字段用于在 csv 文件中与其他对象进行外键关联的指定,具体在 CSV 文件中指定外键关联可参考 关联对象的查询 章节,name字段通常设计为不可更新。 - 通常使用
label字段用于在界面的 Object 显示控件中显示该对象的摘要。 - 通常将
name和label字段均加入 Domain 的inlineSearchColumns属性中。 - 通常将
label字段设置为 Domain 的labelField。 - 通常使用
description字段来存储业务上的描述或帮助信息等。
# enableLogic 约定
对象上名为 enableLogic 的字段指向一个 DynamicLogic,用来判断该对象是否显示或可用。当前带有 enableLogic 字段的只有:
DynamicAction(对象动作):逻辑类型DYNAMIC_ACTION_ENABLE_LOGIC,决定动作是否可用DynamicDashboardWidget(仪表盘小组件):逻辑类型DASHBOARD_WIDGET_ENABLE_LOGIC,决定小组件是否显示
定时任务(DynamicTask)只有核心逻辑(coreLogic),没有启用逻辑;表单字段组(DynamicFormGroup)也没有 enableLogic 字段,逻辑类型 FORM_GROUP_ENABLE_LOGIC 虽然存在(种子数据中也有一条这种类型的动态逻辑),但当前没有任何对象引用它,不会被执行。
# objectType/objectId(s) 约定
针对某一些特定的业务场景,需要在某些 Domain 中记录其关联的对象的类型及 id,通常使用 objectType/objectId(s) 字段的组合来记录。
objectType通常使用objectType字段来记录对象的类型,该字段为指向tech.muyan.DomainClass类型对象的外键引用。objectId(s)通常使用objectId(s)字段来记录对象的一个或者多个 id,如果有多个 id 需要记录,使用逗号分隔的 id 列表形式进行存储。
该约定在系统中的使用举例如下:
tech.muyan.message.Message中的objectType和objectIds字段用于记录该消息所关联的对象的类型及 id。tech.muyan.comment.DomainComment中的objectType和objectId字段用于记录该评论所关联的对象的类型及 id。tech.muyan.dynamic.hook.DynamicObjectHook中的objectType字段用于记录该客制化钩子所关联的对象的类型。
# isSystem 约定
针对某些数据,是系统运行所必须的,不允许用户进行删除或者修改,通常使用名为 isSystem 的 boolean 字段进行标识
系统提供了名为 Has isSystem before Delete 的动态逻辑(逻辑类型 OBJECT_DYNAMIC_HOOK,代码文件为 groovy/objectHooks/beforeDeleteObjectWithIsSystem.groovy),配合 BEFORE_DELETE 的对象客制化使用:删除对象前,如果 isSystem 为 true,则不允许删除。该检查只在非开发环境生效,开发环境(development)下仍然可以删除。
系统已为 DynamicAction、DynamicLogic、DynamicFilter、DynamicPlugin、DynamicConfig 配置了这项检查。