特别注意

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

动态领域模型的回收站、历史版本及评论支持正在开发中。

# 领域模型相关开发

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

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

  • 领域模型的回收站支持
  • 领域模型的多版本支持

# 目标读者

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

# 前置知识

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

# 平台相关元数据

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

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

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

样例如下:

@ManagedEntity
class DynamicDashboardWidget implements MultiTenant<DynamicDashboardWidget>,
  Auditable, Stampable<DynamicDashboardWidget>, 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,而 UserDynamicConfig 之 间是 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 与 domain 关联

# Form 定义

  • 回收站模型中,foreignkeys 字段是一个 JSON 对象,因此在 form field 中,需要将 其前台显示控件设置为 json 类型,可在 DomainColumnClientSideTypeConfig 中创建 记录以对此进行设定。

# API 支持

如果需要在客制化代码中将对象放入回收站, 请使用如下的代码:

import grails.core.GrailsApplication

GrailsApplication grailsApplication = (application as GrailsApplication)
DomainDataService domainDataService = (grailsApplication.getMainContext().getBean("domainDataService") as DomainDataService)

//domainDataService.deleteDomainObjWithHook(GormEntity<?> domainObj, 
//    GrailsUser grailsUser, 
//    GrailsApplication application, 
//    TransactionStatus ts, 
//    ClassLoader classLoader)
domainDataService.deleteDomainObjWithHook(domainObj, grailsUser, application, ts, classLoader)
1
2
3
4
5
6
7
8
9
10
11

注意

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

# 历史版本支持

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

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

# 模型定义

# 待版本化模型

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

tech.muyan.spec.HasRevision<?>

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

其中定义了如下的属性:

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

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

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

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

  • getAllRevisions(): 获取当前对象的所有历史版本, 按照版本号从大到小排列
  • getBumpRevisionPropertyNames(): 定义需要升版本的字段列表

# 历史版本模型

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

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

需要实现如下的 trait:

tech.muyan.spec.DomainRevision<?>

其中定义了如下的属性:

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

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

  • mainObject: 到当前版本的引用

# Dynamic Action 关联

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

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

具体关联的操作,请参考 Action 与 domain 关联

# Form 定义

# 待版本化模型

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

  • revisions

# 历史版本模型

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

  • mainObject

# 评论支持

系统提供了针对 Domain 对象的评论功能,可以通过如下的方式为 Domain 对象增加评论功能:

# Domain 定义

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

# 表单定义

按照实际场景需要,在该 Domain 的列表和更新表单定义中,增加 comments 字段,该字 段在前端,会以一个在右边停靠的评论抽屉的形式展现出来

# 权限说明

默认情况下,系统对于评论的创建、修改、删除、和完成操作的权限控制如下:

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

# 系统中的实际案例

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

# 业务场景

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

# Domain 定义

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

# DynamicLogic

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

import tech.muyan.spec.DomainRevision
import tech.muyan.spec.HasRevision
import tech.muyan.spec.Trashable

@ManagedEntity
class DynamicLogic implements MultiTenant<DynamicLogic>,
  Auditable, Stampable<DynamicLogic>, Serializable,
  HasRevision<DynamicLogic>, // --> 声明支持版本化
  Trashable<DynamicLogic>,  // --> 声明支持回收站
  HasComment<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<DomainRevision<DynamicLogicRevision>> getAllRevisions() {
    revisions.sort((r1, r2) -> {
      if (r1.revision == r2.revision) {
        return 0
      } else if (r1.revision > r2.revision) {
        return 1
      }
      return -1
    }) as Collection<DomainRevision<DynamicLogicRevision>>
  }

  @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

# DynamicLogicRevision

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

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

# 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 包括

  • 从回收站中恢复已删除对象
  • 将历史版本设置为最新版本
  • 比较两个历史版本的差异
  • 比较历史版本与当前版本的差异
;; 文件 DynamicActionDomainClass.csv
RestoreFromTrash,DynamicLogicTrash,1
RevertRevisionToMainObject,DynamicLogicRevision,1
CompareTwoRevisions,DynamicLogicRevision,10
CompareWithMainObject,DynamicLogicRevision,20
1
2
3
4
5

# Form 定义

# DynamicForm.csv

;; 文件 DynamicForm.csv
; Dynamic Logic Revision
$ROOT_ORG$,Show dynamic logic revisions,,,DynamicLogicRevision,LIST,,ROLE_DEVELOPER,
$ROOT_ORG$,Dynamic logic revision detail,,,DynamicLogicRevision,UPDATE,,ROLE_DEVELOPER,

; Dynamic Logic Trash bin
$ROOT_ORG$,Show dynamic logic trash bin,,,DynamicLogicTrash,LIST,Dynamic Logic Trash,ROLE_DEVELOPER,
1
2
3
4
5
6
7

# DynamicFormField.csv

;; Dynamic Logic revision details
Dynamic logic revision detail,id,0,Id,,STATIC_FIELD,,,
Dynamic logic revision detail,name,1,Name,Name of this dynamic logic,STATIC_FIELD,,,
Dynamic logic revision detail,mainObject,3,Main object,Current main object of this revision,STATIC_FIELD,,,
Dynamic logic revision detail,logicType,4,Logic Type,Type of the logic,STATIC_FIELD,,,
Dynamic logic revision detail,description,5,Description,Describe reason & background etc of this dynamic logic,STATIC_FIELD,,,
Dynamic logic revision detail,revision,7,Revision,,STATIC_FIELD,,,
Dynamic logic revision detail,code,10,Code,"Code(written in groovy), refer to https://docs.muyan.tech/zh/cookbook/ for documentation, can use Ctrl + Shift + B to reformat code",STATIC_FIELD,,,"{""codeLanguage"": ""groovy"", ""hasDetailPanel"": true}"
Dynamic logic revision detail,revisions,20,Revisions,,STATIC_FIELD,,,
1
2
3
4
5
6
7
8
9

# Domain Instance 动态字段支持

系统可以创建与特定 Domain Instance 关联的动态字段,具体步骤如下

  1. 实现接口 tech.muyan.dynamic.meta.DynamicEntityType, 该 Domain 是决定对象中 包含哪些动态字段的属性,在创建动态字段实例时,需要通过指定某个 DynamicEntityTypename 属性,与其关联。
  2. 实现接口 tech.muyan.dynamic.meta.DynamicEntityInstance, 该 Domain 是实际包 含动态字段的值的对象,具体包含哪些动态字段由其中的 dynamicEntityType 字段决 定,该字段的值为上一步实现的 DynamicEntityType 的实例。

只需要在创建 DynamicFieldInstance 时,选择与之关联的 DynamicEntityType 对象的类 型,并指定其 name 属性即可。

在创建 DynamicFieldInstance 时,指定其 dynamicEntityType 属性后,系统会在界面 上,自动显示出所有与其关联的动态字段,用户可以在界面上进行相应字段的值编辑。

系统内置了DynamicEntityTypeDynamicEntityInstance 的默认实现,分别为 DefaultDynamicEntityTypeDefaultDynamicEntityInstance,可以基于这两个类, 创建动态字段。

其实现分别如下

  • DefaultDynamicEntityType
package tech.muyan.dynamic.meta

import grails.gorm.MultiTenant
import grails.gorm.hibernate.annotation.ManagedEntity
import tech.muyan.auditable.Auditable
import tech.muyan.auditable.Stampable

@ManagedEntity
class DefaultDynamicEntityType implements MultiTenant<DefaultDynamicEntityType>,
    Auditable, Stampable<DefaultDynamicEntityType>, Serializable, DynamicEntityType<DefaultDynamicEntityType> {

  @SuppressWarnings("unused")
  private static final long serialVersionUID = 1L

  String tenant

  static mapping = {
    tenant index: 'default_dynamic_entity_type_definition_tenant_idx'
    tenantId name: 'tenant'
    cache true
  }

  static constraints = {
    // Name 字段用于用户创建动态字段实例时,与某个 DefaultDynamicEntityType 的具体对象关联
    name nullable: false, blank: false, unique: ['tenant']
  }

  static inlineSearchColumns = ['name']

  static labelField = 'name'

}

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
  • DefaultDynamicEntityInstance
package tech.muyan.dynamic.meta

import grails.gorm.MultiTenant
import grails.gorm.hibernate.annotation.ManagedEntity
import tech.muyan.auditable.Auditable
import tech.muyan.auditable.Stampable

@ManagedEntity
class DefaultDynamicEntityInstance implements MultiTenant<DefaultDynamicEntityInstance>,
    Auditable, Stampable<DefaultDynamicEntityInstance>, Serializable, DynamicEntityInstance<DefaultDynamicEntityInstance, DefaultDynamicEntityType> {

  @SuppressWarnings("unused")
  private static final long serialVersionUID = 1L

  String tenant

  static mapping = {
    tenant index: 'sample_entity_instance_definition_tenant_idx'
    tenantId name: 'tenant'
    cache true
  }

  static constraints = {
    // dynamicEntityType 字段的值决定了该对象会包含哪些动态字段
    dynamicEntityType nullable: false, updatable: false
  }

}
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

# Domain 设计相关约定

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

Last Updated: 2024/12/4 13:00:56