# 动态领域模型

本系统基于 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.csvDomainClassField_example.csv),导入规则见 数据导入

模型定义(DomainClass):只需要填写简略名称,其余名称由系统自动生成。

shortName(*),extInfo,createRoleRequirement.name,readRoleRequirement.name,updateRoleRequirement.name,deleteRoleRequirement.name
SampleDynamicOrderDomain,,DEVELOPER,DEVELOPER,DEVELOPER,DEVELOPER
1
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,,,
1
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

自动生成的模型名称

字段的数据库列名由字段名称转为小写下划线形式(例如 orderDateorder_date);如果结果是 PostgreSQL 关键字(如 userordergroup),会加 _col 后缀;列名最长 63 个字符。

# 领域模型字段属性设置

# 字段属性设置

字段名 描述
名称(name) 字段的名称,用于在代码中引用该字段。在同一领域模型中,字段名称不能重复。创建后不可修改
数据类型(dataType) 字段的数据类型,详细类型见下文。创建后不可修改
字段关联类型(referenceDomain) 数据类型为 DOMAIN_OBJECTDOMAIN_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 中设置 precisionscale
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"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • BIG_DECIMALscale 表示小数位数,precision 表示精度(整数位数 + 小数位数)。两项都设置且都不为 0 时才会建成 decimal(precision, scale) 列;只设置其中一项,或 scale 设为 0 时,按不限精度的 decimal 建列(因此需要整数时请改用 INTEGERLONG)。
  • ENUM / ENUM_LISTenumClass 为 Java 枚举类的完整类名,该类可以放在动态插件中。
  • MAPPED_DOMAIN_OBJECT / MAPPED_DOMAIN_OBJECT_COLLECTIONmappedBy 为反向引用的字段名,必须设置。

# 领域模型元数据

领域模型扩展元数据在模型的 extInfo 中配置:

  1. labelField:在前端的对象控件上,显示对象的哪个属性作为标识;不设置时显示 id
  2. inlineSearchColumns:在前端的对象输入控件中快捷搜索对象时,搜索哪些属性。
  3. loadAfter:导入种子数据时,该类型的对象需要在哪些类型的对象之后导入。具体描述请参考 导入顺序
  4. queryField:CSV 导入时,关联到该模型的列如果没有在列名中指定查询字段,就用该字段查找关联对象。
  5. 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"
}
1
2
3
4
5
6
7
8
9
10
11
12
13

提示

当前版本的 extInfo 还能解析 dynamicTemplatedynamicEntityInstance 两个配置(自定义实体模板),但平台中没有功能读取它们,配置后不会生效。

# 索引

动态模型的索引通过内置的动态模型 DomainClassIndex 定义,其字段如下:

字段 说明
name 索引名称
domainClass 索引所属的领域模型
fields 参与索引的字段名称,JSON 数组,例如 ["orderId", "orderDate"]
unique 是否唯一

创建 DomainClassIndex 对象时,系统会在模型的数据表上建立名为 <表名>_<索引名称小写> 的索引;unique 为 true 时建立同名的 UNIQUE 约束。fields 为空会报 IndexFieldsCouldNotBeEmpty(错误码 14004),字段名称在模型中不存在也会报错。

菜单中没有 DomainClassIndex 的入口,需要通过种子数据或接口创建,需要 DEVELOPER 权限。

提示

删除 DomainClassIndex 对象不会删除数据库中已建立的索引或约束。

# 模型扩展

一个动态模型可以通过 ExtendedDomainClass(字段 parentchild)声明扩展另一个模型:子模型会继承父模型的全部字段,与自身字段合并使用。

ExtendedDomainClass 没有菜单入口,四项权限要求也都为空,界面和接口都不能创建,只能通过种子数据(文件名以 ExtendedDomainClass 开头的 CSV)导入,系统会在导入 DomainClassDomainClassField 之后导入它。

约束如下:

  • 父子模型中同名字段的数据类型必须相同,否则报错;
  • 不允许循环扩展(A 扩展 B,B 又扩展 A);
  • 父模型中如果有 MAPPED_DOMAIN_OBJECTMAPPED_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: "'{ }'"
  }
  // ....
}
1
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 字段。

  1. 通常使用 name 字段用于在 csv 文件中与其他对象进行外键关联的指定,具体在 CSV 文件中指定外键关联可参考 关联对象的查询 章节,name 字段通常设计为不可更新。
  2. 通常使用 label 字段用于在界面的 Object 显示控件中显示该对象的摘要。
  3. 通常将 namelabel 字段均加入 Domain 的 inlineSearchColumns 属性中。
  4. 通常将 label 字段设置为 Domain 的 labelField
  5. 通常使用 description 字段来存储业务上的描述或帮助信息等。

# enableLogic 约定

对象上名为 enableLogic 的字段指向一个 DynamicLogic,用来判断该对象是否显示或可用。当前带有 enableLogic 字段的只有:

  1. DynamicAction(对象动作):逻辑类型 DYNAMIC_ACTION_ENABLE_LOGIC,决定动作是否可用
  2. 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 中的 objectTypeobjectIds 字段用于记录该消息所关联的对象的类型及 id。
  • tech.muyan.comment.DomainComment 中的 objectTypeobjectId 字段用于记录该评论所关联的对象的类型及 id。
  • tech.muyan.dynamic.hook.DynamicObjectHook 中的 objectType 字段用于记录该客制化钩子所关联的对象的类型。

# isSystem 约定

针对某些数据,是系统运行所必须的,不允许用户进行删除或者修改,通常使用名为 isSystemboolean 字段进行标识

系统提供了名为 Has isSystem before Delete 的动态逻辑(逻辑类型 OBJECT_DYNAMIC_HOOK,代码文件为 groovy/objectHooks/beforeDeleteObjectWithIsSystem.groovy),配合 BEFORE_DELETE 的对象客制化使用:删除对象前,如果 isSystem 为 true,则不允许删除。该检查只在非开发环境生效,开发环境(development)下仍然可以删除。

系统已为 DynamicActionDynamicLogicDynamicFilterDynamicPluginDynamicConfig 配置了这项检查。

Last Updated: 2026/9/23 17:28:40