特别注意
使用本方式定义领域模型已经废弃,如果您要创建的领域模型不需要对象的 回收站支持、历史版本支持 及对象评论支持, 请使用新的 动态领域模型定义 方式定义领域模型。
动态领域模型的回收站、历史版本及评论支持正在开发中。
# 领域模型相关开发
本系统的领域模型以 Grails 的 Domain 定义,使用 Groovy 语言编写,使用 GORM 作为 ORM 框架,底层使用 Hibernate。
以下描述了,领域模型支持相关功能的一些开发说明,当前包括
- 领域模型的回收站支持
- 领域模型的多版本支持
# 目标读者
本文档的目标读者为:本系统的开发人员
# 前置知识
读者需要对 Hibernate, GORM, Grails 有一定的了解。如下是相关的参考资料:
# 平台相关元数据
通过 Domain 中静态的
labelField
属性指定在前端的对象控件上,显示对象的哪个属性 作为标识。通过 Domain 中静态的
inlineSearchColumn
属性指定在前端的对象输入控件中,快捷搜 索对象时,搜索哪些属性。通过 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'
// ...
}
2
3
4
5
6
7
8
9
10
11
# 回收站支持
领域模型的回收站支持主要包括如下功能
- 对象删除时,系统不进行物理删除,而是将对象移动到回收站中
- 回收站中的对象可以进行恢复,恢复时其外键关联也会一并被恢复
数据丢失警告
回收收操作会首先将对象从待回收模型的数据库表中删除,然后将其保存到回收站模型 的数据库表中。因此如果待回收对象的外键关联定义了级联删除,则回收时,该关联对 象会被物理删除,在后续恢复时,该关联对象将无法被恢复,造成数据丢失, 需要注意。
# 模型定义
# 待回收模型
回收站支持的模型定义,需要实现如下的 trait:
tech.muyan.spec.Trashable<?>
当模型继承了该 trait 后,在对对象作删除操作时,系统会将其放入回收站中,而不是进行物理删除。
# 回收站模型
回收站模型定义,需要遵循如下的命名规则:
待回收模型名 + Trash
,如User
的 回收站模型为UserTrash
。回收站模型中,需要包含待回收模型中的所有属性,以避免任何属性丢失。
回收站模型定义,需要实现如下的 trait:
tech.muyan.spec.DomainTrash
其中定义了如下的属性:
originalId
: 原始对象的 ID, 供参考用trashedBy
: 删除该对象的用户 IDdateTrashed
: 删除对象的时间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
}
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)
2
3
4
5
6
7
8
9
10
11
注意
不能直接调用 Grails domain 对象的 delete 方法对对象进行直接删除
# 历史版本支持
领域模型的历史版本支持主要包括如下功能:
- 对象保存时,系统会根据需要升版本的字段列表配置,如有必要,会自动保存一个历史版本
- 将历史版本与当前版本进行比较,查看历史版本与当前版本的差异
- 比较两个历史版本,查看历史版本之间的差异
- 将某个历史版本恢复为当前版本
# 模型定义
# 待版本化模型
版本化支持的模型定义,需要实现如下的 trait:
tech.muyan.spec.HasRevision<?>
当模型继承了该 trait 后,该模型即具备了版本化功能。
其中定义了如下的属性:
revision
: 当前对象的版本号,从 1 开始,每次保存时,如果有需要升版本的字段发生变 化,版本号会自动加一。如果没有需要升版本的字段发生变化,版本号不会变化。
另外,模型中还要定义如下的属性:
revisions
: 用于指向所有历史版本的集合,该属性应该是一个指向tech.muyan.spec.DomainRevision<?>
的OneToMany
关联
其中定义了如下的接口方法,需要实现:
getAllRevisions()
: 获取当前对象的所有历史版本, 按照版本号从大到小排列getBumpRevisionPropertyNames()
: 定义需要升版本的字段列表
# 历史版本模型
历史版本模型定义,需要遵循如下的命名规则:
待版本化模型名 + Revision
, 如待版 本化模型为DynamicLogic
, 则历史版本模型为DynamicLogicRevision
历史版本模型中,需要包含主模型中的所有属性,以避免升版时,任何属性无法保存导 致数据丢失。
需要实现如下的 trait:
tech.muyan.spec.DomainRevision<?>
其中定义了如下的属性:
revision
: 该历史版本的版本号
另外,历史版本模型中,还需要定义如下的属性,用于保存到当前版本的引用:
mainObject
: 到当前版本的引用
# Dynamic Action 关联
平台定义了如下的 Action,用于支持领域模型的历史版本功能:
RevertRevisionToMainObject
应定义在历史版本模型上,将历史版本恢复为当前版本CompareTwoRevisions
应定义在历史版本模型上,用于比较两个历史版本CompareWithMainObject
应定义在历史版本模型上,比较历史版本与当前版本
具体关联的操作,请参考 Action 与 domain 关联
# Form 定义
# 待版本化模型
待版本化模型的 form 中,如果要显示其所有历史版本的列表,需要包含如下字段:
revisions
# 历史版本模型
历史版本模型的 form 中,如果需要显示其对应的当前版本的对象,需要包含如下字段:
mainObject
# 评论支持
系统提供了针对 Domain 对象的评论功能,可以通过如下的方式为 Domain 对象增加评论功能:
# Domain 定义
- 在 Domain Class 定义上增加
HasComment<?>
trait, 其中定义了getComments()
方法,用于获取该对象的所有评论 - 在 Domain Class 中的字段定义中,增加如下的字段:
List<DomainComment> comments
- 修改其
HasMany
部分定义,增加如下部分comments : DomainComment
- 修改其
fetchMode
部分定义,增加如下部分comments : org.hibernate.FetchMode.JOIN
# 表单定义
按照实际场景需要,在该 Domain 的列表和更新表单定义中,增加 comments
字段,该字
段在前端,会以一个在右边停靠的评论抽屉的形式展现出来
# 权限说明
默认情况下,系统对于评论的创建、修改、删除、和完成操作的权限控制如下:
- 默认所有用户均可针对其可见的主对象,创建评论
- 评论的创建者,可以修改和删除其评论
- 评论所关联的主对象的创建者,可以删除针对该主对象的评论
- 评论的创建者和评论所关联的主对象的创建者,可以将某评论标记为完成
# 系统中的实际案例
以下以一个平台中的实际案例为例,介绍如何为 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 属性发生变化时,才会升版
}
}
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 中还要包括主对像中的所有非外键字段
}
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 模型定义中还要包括主对像中的所有非外键字段
}
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
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,
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,,,
2
3
4
5
6
7
8
9
# Domain Instance 动态字段支持
系统可以创建与特定 Domain Instance 关联的动态字段,具体步骤如下
- 实现接口
tech.muyan.dynamic.meta.DynamicEntityType
, 该 Domain 是决定对象中 包含哪些动态字段的属性,在创建动态字段实例时,需要通过指定某个DynamicEntityType
的name
属性,与其关联。 - 实现接口
tech.muyan.dynamic.meta.DynamicEntityInstance
, 该 Domain 是实际包 含动态字段的值的对象,具体包含哪些动态字段由其中的dynamicEntityType
字段决 定,该字段的值为上一步实现的DynamicEntityType
的实例。
只需要在创建 DynamicFieldInstance 时,选择与之关联的 DynamicEntityType 对象的类
型,并指定其 name
属性即可。
在创建 DynamicFieldInstance 时,指定其 dynamicEntityType
属性后,系统会在界面
上,自动显示出所有与其关联的动态字段,用户可以在界面上进行相应字段的值编辑。
系统内置了DynamicEntityType
和 DynamicEntityInstance
的默认实现,分别为
DefaultDynamicEntityType
和 DefaultDynamicEntityInstance
,可以基于这两个类,
创建动态字段。
其实现分别如下
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'
}
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
}
}
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 设计相关约定。