# 领域模型相关开发

特别注意

使用本方式定义领域模型已经废弃。如果您要创建的领域模型不需要对象的 回收站支持 和 历史版本支持,请使用新的 动态领域模型定义 方式定义领域模型。

动态领域模型的回收站、历史版本支持正在开发中。对象评论功能当前版本不可用,见 评论支持。

本系统的领域模型以 Grails 的 Domain 定义,使用 Groovy 语言编写,使用 GORM 作为 ORM 框架,底层使用 Hibernate。

以下描述了领域模型支持相关功能的一些开发说明,当前包括:

  • 领域模型的回收站支持
  • 领域模型的多版本支持
  • 领域模型的评论支持(当前版本不可用)

# 目标读者

本文档的目标读者为:本系统的开发人员

# 前置知识

读者需要对 Hibernate, GORM, Grails 有一定的了解。如下是相关的参考资料:

# 平台相关元数据

  1. 通过 Domain 中静态的 labelField 属性指定在前端的对象控件上,显示对象的哪个属性作为标识。

  2. 通过 Domain 中静态的 inlineSearchColumns 属性指定在前端的对象输入控件中,快捷搜索对象时,搜索哪些属性。

  3. 通过 Domain 中静态的 loadAfter 属性指定在导入种子数据时,该对象需要在哪些对象之后导入。具体描述请参考 导入顺序。

样例如下:

@ManagedEntity
class DynamicDashboardWidget implements MultiTenant<DynamicDashboardWidget>,
  Auditable, Serializable {
  // ...
  // 用户快捷搜索时,使用 name 和 label 两个字段进行搜索匹配
  // When user do quick search, use name and label two fields for search matching
  static inlineSearchColumns = ['name', 'label']
  // 界面上显示 Object 控件时,显示其 label 字段的值作为标识
  // When display Object control on the interface, display the value of the label field as the identifier
  static labelField = 'label'
  // ...
}
1
2
3
4
5
6
7
8
9
10
11

# 回收站支持

领域模型的回收站支持主要包括如下功能

  1. 对象删除时,系统不进行物理删除,而是将对象移动到回收站中
  2. 回收站中的对象可以进行恢复,恢复时其外键关联也会一并被恢复

数据丢失警告

回收操作会首先将对象从待回收模型的数据库表中删除,然后将其保存到回收站模型的数据库表中。因此如果待回收对象的外键关联定义了级联删除,则回收时,该关联对象会被物理删除,在后续恢复时,该关联对象将无法被恢复,造成数据丢失,需要注意。

# 模型定义

# 待回收模型

回收站支持的模型定义,需要实现如下的 trait:

tech.muyan.spec.Trashable<?>

当模型继承了该 trait 后,在对对象作删除操作时,系统会将其放入回收站中,而不是进行物理删除。

# 回收站模型

  1. 回收站模型定义,需要遵循如下的命名规则: 待回收模型名 + Trash,如 User 的回收站模型为 UserTrash。

  2. 回收站模型中,需要包含待回收模型中的所有属性,以避免任何属性丢失。

回收站模型定义,需要实现如下的 trait:

tech.muyan.spec.DomainTrash

  • 其中定义了如下的属性:

    • originalId: 原始对象的 ID, 供参考用
    • trashedBy: 删除该对象的用户 ID
    • dateTrashed: 删除对象的时间
    • foreignKeys: JSON 字符串,记录了被删除对象的所有外键关联,用于在恢复时,一并恢复外键关联
  • 其中定义了如下的接口方法,需要实现:

    • abstract Trashable<?> getNullObject() 用于对象被软删除时,填充被删除对象的关联对象中,指向被删除对象的,必填的外键字段的一个占位符对象,该对象需要在数据库中真实存在,但不应该被赋予实际业务含义。

Domain 设计提示

如果待删除的对象是 one to many 关系中的 many 端,那么可以在回收站的 domain 定义中,不包含 one 端的那个关联字段,系统会记录外键并在恢复回收站时,自动处理。

举例,如果待放入回收站的对象是 DynamicConfig,而 User 与 DynamicConfig 之间是 one to many 的关系。

class User {
    String name
    static hasMany = [dynamicConfigs: DynamicConfig]
}

class DynamicConfig {
    String name
    // 在 DynamicConfig 对象被放入回收站时,下述 foreignKey 会被记录在 foreignKeys 字段中
    // 因此如无必要,可以无需在 DynamicConfigTrash 的模型定义中,显式声明
    User user
}
1
2
3
4
5
6
7
8
9
10
11

那么,可以在回收站的 domain 定义 DynamicConfigTrash 中,可以不包含 User 关联关系,因为该关联关系会被系统自动处理,保存在 foreignKeys 字段中。从回收站恢复时,可以恢复该关联关系。

Domain 设计注意

在定义回收站 domain 时候,对于原对象中有唯一校验的字段,如 name, key 等类型字段,可以不加唯一校验,避免出现用户在删除了某对象后,再次创建的新的同名称的对象后,无法被删除的场景。

# Dynamic Action 关联

平台定义了名为 RestoreFromTrash 的 Action,用于支持回收站对象的恢复功能,如果希望支持回收站的恢复功能,需要将该 Action 与回收站模型进行关联。具体关联的操作,请参考 挂载到表单。当前版本中 Action 是关联到表单上的(DynamicActionDynamicForm),因此需要将该 Action 关联到回收站模型的列表表单。

# Form 定义

  • 回收站模型中,foreignKeys 字段是一个 JSON 对象,因此在表单字段中,需要将其显示控件设置为 json:在种子数据 DynamicFormField_with_displayType.csv 中,将该字段的 displayType 列设置为 json。

# API 支持

如果需要在客制化代码中删除对象(对实现了 Trashable 的模型即为放入回收站),请使用 DomainDataAccessService.deleteDomainObjWithHook,它会执行删除前后的对象客制化并处理回收站逻辑。以下代码摘自平台内置的批量删除动作 multipleDeleteLogic.groovy:

import tech.muyan.BeanHelper
import tech.muyan.DomainClass
import tech.muyan.domain.dao.DomainDataAccessService
import tech.muyan.domain.dao.enums.DataAccessResult
import tech.muyan.domain.dao.params.DomainObjectChangeResult
import tech.muyan.domain.dao.params.DomainObjectDeleteParams
import tech.muyan.utils.QueryHelper

DomainDataAccessService dataAccessService = BeanHelper.getBean(DomainDataAccessService)
DomainClass domainClass = objectType as DomainClass

QueryHelper.withTransactionAsArg { ts ->
  objects.forEach { obj ->
    DomainObjectChangeResult result = dataAccessService.deleteDomainObjWithHook(new DomainObjectDeleteParams([
      domainClass      : domainClass,   // 待删除对象的领域模型
      auth             : userContext,   // 当前用户
      transactionStatus: ts,            // 当前事务
      id               : obj.id,        // 待删除对象的 id
    ]))
    if (result.status != DataAccessResult.SUCCESS) {
      // result.status 为 ERROR 或 WARNING 时,result.message 中是具体原因
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

注意

不能直接调用 Grails domain 对象的 delete 方法对对象进行直接删除

# 历史版本支持

领域模型的历史版本支持主要包括如下功能:

  1. 对象保存时,系统会根据需要升版本的字段列表配置,如有必要,会自动保存一个历史版本
  2. 将历史版本与当前版本进行比较,查看历史版本与当前版本的差异
  3. 比较两个历史版本,查看历史版本之间的差异
  4. 将某个历史版本恢复为当前版本

# 模型定义

# 待版本化模型

版本化支持的模型定义,需要实现如下的 trait:

tech.muyan.spec.HasRevision<?>

当模型继承了该 trait 后,该模型即具备了版本化功能。

其中定义了如下的属性:

  • revision: 当前对象的版本号,从 1 开始,每次保存时,如果有需要升版本的字段发生变化,版本号会自动加一。如果没有需要升版本的字段发生变化,版本号不会变化。

另外,模型中还要定义如下的属性:

  • revisions: 用于指向所有历史版本的集合,该属性应该是一个指向历史版本模型(实现 tech.muyan.spec.DomainRevision<?>)的 OneToMany 关联

其中定义了如下的接口方法,需要实现:

  • Collection<GormEntity<?>> getAllRevisions(): 获取当前对象的所有历史版本(DynamicLogic 的实现按版本号从小到大排序)
  • Collection<String> getBumpRevisionPropertyNames(): 定义需要升版本的字段列表

# 历史版本模型

  1. 历史版本模型定义,需要遵循如下的命名规则: 待版本化模型名 + Revision, 如待版本化模型为 DynamicLogic, 则历史版本模型为 DynamicLogicRevision

  2. 历史版本模型中,需要包含主模型中的所有属性,以避免升版时,任何属性无法保存导致数据丢失。

需要实现如下的 trait:

tech.muyan.spec.DomainRevision<?>

其中定义了如下的属性:

  • revision: 该历史版本的版本号

另外,历史版本模型中,还需要定义如下的属性,用于保存到当前版本的引用:

  • mainObject: 到当前版本的引用

# Dynamic Action 关联

平台定义了如下的 Action,用于支持领域模型的历史版本功能:

  • RevertRevisionToMainObject 应定义在历史版本模型上,将历史版本恢复为当前版本
  • CompareTwoRevisions 应定义在历史版本模型上,用于比较两个历史版本
  • CompareWithMainObject 应定义在历史版本模型上,比较历史版本与当前版本

这些 Action 需要关联到历史版本模型的列表表单上,具体关联的操作,请参考 挂载到表单

# Form 定义

# 待版本化模型

待版本化模型的 form 中,如果要显示其所有历史版本的列表,需要包含如下字段:

  • revisions

# 历史版本模型

历史版本模型的 form 中,如果需要显示其对应的当前版本的对象,需要包含如下字段:

  • mainObject

# 评论支持

当前版本不可用

以下描述的对象评论功能在当前版本中无法使用:

  • 评论对象 DomainComment 的创建、读取、修改、删除四项权限要求都为空,任何用户(包括管理员)都不能创建或读取评论;
  • 控制评论修改、删除权限的动态逻辑 DomainCommentCanUpdateDelete(canUpdateDeleteDomainComment.groovy)中的判断代码已被注释,固定返回不可修改、不可删除;
  • 前端没有评论抽屉组件。表单中加入的 comments 字段只按普通的关联列表显示。

下文保留原有的配置方式,仅供维护已有 GORM 模型时参考。

原设计中,可以通过如下的方式为 Domain 对象增加评论功能:

# Domain 定义

  1. 在 Domain Class 定义上增加 tech.muyan.spec.HasComment<?> trait,其中定义了 List<DomainComment> getComments() 方法,用于获取该对象的所有评论
  2. 在 Domain Class 中的字段定义中,增加如下的字段: List<DomainComment> comments
  3. 修改其 HasMany 部分定义,增加如下部分 comments : DomainComment
  4. 修改其 fetchMode 部分定义,增加如下部分 comments : org.hibernate.FetchMode.JOIN

# 表单定义

按照实际场景需要,在该 Domain 的列表和更新表单定义中,增加 comments 字段。原设计中该字段在前端以一个在右边停靠的评论抽屉展现;当前版本的前端没有这个抽屉,只按普通的关联列表显示。

# 权限说明

原设计中,评论的创建、修改、删除和完成操作的权限控制如下:

  1. 所有用户均可针对其可见的主对象创建评论
  2. 评论的创建者可以修改和删除其评论
  3. 评论所关联的主对象的创建者可以删除针对该主对象的评论
  4. 评论的创建者和评论所关联的主对象的创建者可以将某评论标记为完成

当前版本中以上规则都不成立:DomainComment 的权限要求为空,任何用户都不能创建评论;修改、删除权限固定为否。

# 系统中的实际案例

以下以一个平台中的实际案例为例,介绍如何为 Domain 对象增加版本化和回收站的支持。

# 业务场景

DynamicLogic 是开发平台的核心资产,因此希望能够对其进行版本化管理、回收站支持及评论支持,以便于在开发过程中避免任何开发内容的丢失,且支持逻辑出错时快速恢复,同时增加相关的说明。其中评论功能当前版本不可用,见 评论支持。

# Domain 定义

  • DynamicLogic: 主对象
  • DynamicLogicRevision: 历史版本对象
  • DynamicLogicTrash: 回收站对象
  • HasComment: 标识该对象支持评论

# DynamicLogic

本处只列出与回收站、多版本支持及评论支持相关的属性,其他属性请参考平台源代码中 DynamicLogic 的定义。

import org.grails.datastore.gorm.GormEntity
import tech.muyan.spec.HasComment
import tech.muyan.spec.HasRevision
import tech.muyan.spec.Trashable

@ManagedEntity
class DynamicLogic implements MultiTenant<DynamicLogic>,
  Auditable, Serializable,
  HasRevision<DynamicLogic>, // --> 声明支持版本化
  Trashable<DynamicLogic>,  // --> 声明支持回收站
  HasComment<DynamicLogic>, // --> 声明支持评论
  tech.muyan.domain.DynamicLogic {
  
  // ....
  // --> revision 字段从 HasRevision 继承,用于记录当前主对象的版本号
  
  // Comments 字段,记录当前主对象的所有评论
  List<DomainComment> comments
  
  static fetchMode = [
    // .....
    
    // Comments, 使用 JOIN 方式查询
    comments: org.hibernate.FetchMode.JOIN,
  ]
  
  static hasMany = [
    // .....

    // Revisions
    revisions: DynamicLogicRevision, // --> 历史版本列表
    comments : DomainComment,        // --> 评论列表,与主对象是一对多关系
  ]
  
  static mappedBy = [ // --> 与历史版本的映射  
    revisions    : 'mainObject' // --> 历史版本中的主对象引用为 mainObject 字段
  ]
  
  @Override // --> 获取经过排序的历史版本列表,按版本号从小到大排序
  Collection<GormEntity<?>> getAllRevisions() {
    revisions.sort((r1, r2) -> {
      if (r1.revision == r2.revision) {
        return 0
      } else if (r1.revision > r2.revision) {
        return 1
      }
      return -1
    })
  }

  @Override
  Collection<String> getBumpRevisionPropertyNames() { // --> 获取升版的属性列表
    return ['code']  // --> 只有 code 属性发生变化时,才会升版
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# DynamicLogicRevision

本处只列出与多版本支持相关的属性,其他属性请参考平台源代码中 DynamicLogicRevision 的定义。

@ManagedEntity
class DynamicLogicRevision implements   // --> 命名规范: 主模型类名 + Revision
  DomainRevision<DynamicLogicRevision>, // --> 声明为历史版本对象
  MultiTenant<DynamicLogicRevision>, Auditable {
  
  DynamicLogic mainObject  // --> 历史版本的主对象
  
  // --> revision 字段从 DomainRevision 继承,用于记录历史版本的版本号
  // --> DomainRevision 中还要包括主对象中的所有非外键字段
}
1
2
3
4
5
6
7
8
9
10

# DynamicLogicTrash

本处只列出与回收站相关的属性,其他属性请参考平台源代码中 DynamicLogicTrash 的定义。

@ManagedEntity
class DynamicLogicTrash implements // --> 命名规范: 主模型类名 + Trash
  DomainTrash<DynamicLogicTrash>,  // --> 声明为回收站对象
  MultiTenant<DynamicLogicTrash> {
  
  static constraints = {
    foreignKeys nullable: true, blank: false // --> 将外键字段设置为可以为 null, 但是不可以为空字符串
  }

  static mapping = {    
    // --> 外键字段的类型定义,使用 JSON 格式存储,读取到 Java 中时,使用 String 类型
    foreignKeys type: 'tech.muyan.rdbms.postgres.JSONBType', sqlType: "jsonb", defaultValue: "'{ }'"
  }

  Trashable<DynamicLogic> getNullObject() { 
    // --> 用于回收主对象时,设置到主对象的外键关联为该 NullObject, 避免出现外键约束错误
    return DynamicLogic.findByName("Dummy Dynamic Logic")
  }

  static transients = ['nullObject'] // --> nullObject 字段(对应 getNullObject 方法) 不需要持久化到数据库中
  
  // --> DomainTrash 模型定义中还要包括主对象中的所有非外键字段  
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Action 关联

系统内置可用的 Action 包括

  • 从回收站中恢复已删除对象
  • 将历史版本设置为最新版本
  • 比较两个历史版本的差异
  • 比较历史版本与当前版本的差异
;; 文件 DynamicActionDynamicForm.csv
action.name(*),form.name(*),displaySequence,group.name
RevertRevisionToMainObject,Show dynamic logic revisions,1,
CompareTwoRevisions,Show dynamic logic revisions,10,
CompareWithMainObject,Show dynamic logic revisions,20,
RestoreFromTrash,Show dynamic logic trash bin,1,
1
2
3
4
5
6

# Form 定义

# DynamicForm.csv

;; 文件 DynamicForm.csv
name(*),label,description,objectType.shortName(*),type.name(*),formHook.name,formHookTriggerFields,dataHook.name,extInfo,accessRequirement.name
; Dynamic Logic Revision
Show dynamic logic revisions,,,DynamicLogicRevision,LIST,,,,,DEVELOPER
Dynamic logic revision detail,,,DynamicLogicRevision,UPDATE,,,,,DEVELOPER

; Dynamic Logic Trash bin
Show dynamic logic trash bin,,,DynamicLogicTrash,LIST,,,,,DEVELOPER
1
2
3
4
5
6
7
8

最后一列 accessRequirement.name 是可以访问该表单的权限要求(RoleRequirement 名称)。

# DynamicFormField_with_displayType.csv

;; 文件 DynamicFormField_with_displayType.csv
form.name(*),fieldName(*),label,displaySequence,helpText,fieldType,nullable,group.name,extInfo,displayType
Dynamic logic revision detail,id,Id,0,,STATIC_FIELD,,,,
Dynamic logic revision detail,name,Name,1,Name of this dynamic logic,STATIC_FIELD,,,,
Dynamic logic revision detail,mainObject,Main object,3,Current main object of this revision,STATIC_FIELD,,,,
Dynamic logic revision detail,logicType,Logic Type,4,Type of the logic,STATIC_FIELD,,,,
Dynamic logic revision detail,description,Description,5,Describe reason & background etc of this dynamic logic,STATIC_FIELD,,,,
Dynamic logic revision detail,revision,Revision,7,,STATIC_FIELD,,,,
Dynamic logic revision detail,code,Code,10,"Code(written in groovy), refer to https://docs.muyan.io/zh/cookbook/ for documentation, can use Ctrl + Shift + B to reformat code",STATIC_FIELD,,,"{""codeLanguage"": ""groovy"", ""hasDetailPanel"": true}",functionEditor
1
2
3
4
5
6
7
8
9

# Domain Instance 动态字段支持

1.0 起已移除(DefaultDynamicEntityType / DefaultDynamicEntityInstance 及动态字段定义、实例模型已删除;需要按业务扩展字段时,请使用 动态领域模型)。

# Domain 设计相关约定

本部分内容请参考 Domain 设计相关约定。

Last Updated: 2026/9/24 14:27:35