# Domain Model Development
Important
Defining domain models this way is deprecated. If the domain model you want to create does not need trash bin support or revision history support for its objects, define it with the new dynamic domain model approach instead.
Trash bin and revision history support for dynamic domain models is under development. Object comments are not available in the current version; see Comment Support.
Domain models in this system are defined as Grails Domain classes, written in Groovy, with GORM as the ORM framework and Hibernate underneath.
The following sections describe how to develop features supported by domain models. They currently cover:
- Trash bin support for domain models
- Multi-revision support for domain models
- Comment support for domain models (not available in the current version)
# Target Audience
This document is intended for developers of this system.
# Prerequisites
Readers should have some familiarity with Hibernate, GORM and Grails. The following references are relevant:
# Platform Metadata
The static
labelFieldproperty of a Domain specifies which property of the object is displayed as its identifier in frontend object controls.The static
inlineSearchColumnsproperty of a Domain specifies which properties are searched when quickly searching for objects in frontend object input controls.The static
loadAfterproperty of a Domain specifies which objects must be imported before this object when seed data is imported. For details, see Import Order.
Example:
@ManagedEntity
class DynamicDashboardWidget implements MultiTenant<DynamicDashboardWidget>,
Auditable, Serializable {
// ...
// Quick search by the user matches against the name and label fields
static inlineSearchColumns = ['name', 'label']
// When an Object control is shown in the UI, the value of its label field is displayed as the identifier
static labelField = 'label'
// ...
} 2
3
4
5
6
7
8
9
# Trash Bin Support
Trash bin support for domain models mainly provides the following features:
- When an object is deleted, the system does not physically delete it; instead it moves the object to the trash bin
- Objects in the trash bin can be restored, and their foreign key associations are restored along with them
Data Loss Warning
The trash operation first deletes the object from the database table of the trashable model, and then saves it to the database table of the trash model. Therefore, if a foreign key association of the object being trashed is defined with cascade delete, the associated object will be physically deleted when the object is trashed, and it cannot be restored later, causing data loss. Keep this in mind.
# Model Definition
# Trashable Model
A model that supports the trash bin must implement the following trait:
tech.muyan.spec.Trashable<?>
Once a model implements this trait, deleting one of its objects puts the object into the trash bin instead of physically deleting it.
# Trash Model
The trash model must follow this naming rule:
trashable model name + Trash; for example, the trash model ofUserisUserTrash.The trash model must contain all properties of the trashable model, so that no property is lost.
The trash model must implement the following trait:
tech.muyan.spec.DomainTrash
It defines the following properties:
originalId: ID of the original object, for referencetrashedBy: ID of the user who deleted the objectdateTrashed: time the object was deletedforeignKeys: JSON string recording all foreign key associations of the deleted object, used to restore those associations on restore
It defines the following interface method, which must be implemented:
abstract Trashable<?> getNullObject()returns a placeholder object used when the object is soft-deleted: required foreign key fields in associated objects that point to the deleted object are filled with it. This object must actually exist in the database, but should not carry any real business meaning.
Domain Design Tip
If the object to be deleted is the many side of a one-to-many relationship, the trash domain definition can omit the association field to the one side; the system records the foreign key and handles it automatically when the object is restored from the trash bin.
For example, suppose the object to be put into the trash bin is DynamicConfig, and User has a one-to-many relationship with DynamicConfig.
class User {
String name
static hasMany = [dynamicConfigs: DynamicConfig]
}
class DynamicConfig {
String name
// When a DynamicConfig object is put into the trash bin, the foreign key below is recorded in the foreignKeys field
// so unless necessary, it does not need to be explicitly declared in the DynamicConfigTrash model definition
User user
}
2
3
4
5
6
7
8
9
10
11
In that case, the trash domain definition DynamicConfigTrash can leave out the User association, because the system handles it automatically and stores it in the foreignKeys field. The association can be restored when the object is restored from the trash bin.
Domain Design Note
When defining a trash domain, fields that have a uniqueness constraint in the original object, such as name or key, can be left without the uniqueness constraint. This avoids the situation where a user deletes an object, creates a new object with the same name, and then cannot delete the new one.
# Dynamic Action Association
The platform defines an Action named RestoreFromTrash to restore objects from the trash bin. To support restoring from the trash bin, associate this Action with the trash model. For how to associate it, see Attach to Forms. In the current version, Actions are associated with forms (DynamicActionDynamicForm), so associate this Action with the list form of the trash model.
# Form Definition
- In the trash model, the
foreignKeysfield is a JSON object, so its display control in the form field must be set tojson: in the seed data fileDynamicFormField_with_displayType.csv, set thedisplayTypecolumn of this field tojson.
# API Support
To delete an object in custom code (for a model that implements Trashable, this means putting it into the trash bin), use DomainDataAccessService.deleteDomainObjWithHook. It runs the object hooks before and after deletion and handles the trash bin logic. The following code is taken from the platform's built-in bulk delete action 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, // Domain model of the object to delete
auth : userContext, // Current user
transactionStatus: ts, // Current transaction
id : obj.id, // id of the object to delete
]))
if (result.status != DataAccessResult.SUCCESS) {
// When result.status is ERROR or WARNING, result.message contains the reason
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Note
Do not call the delete method of a Grails domain object to delete the object directly
# Revision History Support
Revision history support for domain models mainly provides the following features:
- When an object is saved, the system automatically saves a history revision if needed, based on the configured list of fields that bump the revision
- Compare a history revision with the current revision to see the differences between them
- Compare two history revisions to see the differences between them
- Restore a history revision as the current revision
# Model Definition
# Versioned Model
A model that supports revisions must implement the following trait:
tech.muyan.spec.HasRevision<?>
Once a model implements this trait, it supports revisions.
It defines the following property:
revision: revision number of the current object, starting from 1. On each save, if any field that bumps the revision has changed, the revision number is automatically incremented by one. If no such field has changed, the revision number stays the same.
In addition, the model must define the following property:
revisions: collection pointing to all history revisions. This property should be aOneToManyassociation to the history revision model (which implementstech.muyan.spec.DomainRevision<?>)
It defines the following interface methods, which must be implemented:
Collection<GormEntity<?>> getAllRevisions(): returns all history revisions of the current object (theDynamicLogicimplementation sorts them by revision number in ascending order)Collection<String> getBumpRevisionPropertyNames(): defines the list of fields that bump the revision
# History Revision Model
The history revision model must follow this naming rule:
versioned model name + Revision; for example, if the versioned model isDynamicLogic, the history revision model isDynamicLogicRevisionThe history revision model must contain all properties of the main model, so that no data is lost because some property cannot be saved when a revision is created.
It must implement the following trait:
tech.muyan.spec.DomainRevision<?>
It defines the following property:
revision: revision number of this history revision
In addition, the history revision model must define the following property to hold a reference to the current revision:
mainObject: reference to the current revision
# Dynamic Action Association
The platform defines the following Actions to support revision history for domain models:
RevertRevisionToMainObjectshould be defined on the history revision model; it restores a history revision as the current revisionCompareTwoRevisionsshould be defined on the history revision model; it compares two history revisionsCompareWithMainObjectshould be defined on the history revision model; it compares a history revision with the current revision
These Actions must be associated with the list form of the history revision model. For how to associate them, see Attach to Forms
# Form Definition
# Versioned Model
To display the list of all history revisions in a form of the versioned model, the form must include the following field:
revisions
# History Revision Model
To display the corresponding current-revision object in a form of the history revision model, the form must include the following field:
mainObject
# Comment Support
Not Available in the Current Version
The object comment feature described below cannot be used in the current version:
- The create, read, update and delete permission requirements of the comment object
DomainCommentare all empty, so no user (including administrators) can create or read comments; - In the dynamic logic
DomainCommentCanUpdateDelete(canUpdateDeleteDomainComment.groovy), which controls the update and delete permissions of comments, the check code has been commented out, and it always returns not updatable and not deletable; - The frontend has no comment drawer component. A
commentsfield added to a form is displayed only as an ordinary association list.
The original configuration approach is kept below for reference only, when maintaining existing GORM models.
In the original design, comments could be added to Domain objects as follows:
# Domain Definition
- Add the
tech.muyan.spec.HasComment<?>trait to the Domain Class definition. It defines theList<DomainComment> getComments()method, which returns all comments of the object - Add the following field to the field definitions of the Domain Class:
List<DomainComment> comments - Modify its
HasManydefinition and addcomments : DomainComment - Modify its
fetchModedefinition and addcomments : org.hibernate.FetchMode.JOIN
# Form Definition
Depending on the actual scenario, add the comments field to the list and update form definitions of the Domain. In the original design, this field was displayed in the frontend as a comment drawer docked on the right; the current frontend has no such drawer and displays it only as an ordinary association list.
# Permissions
In the original design, permissions for creating, updating, deleting and completing comments were controlled as follows:
- All users can create comments on main objects visible to them
- The creator of a comment can update and delete it
- The creator of the main object a comment is associated with can delete comments on that main object
- The creator of a comment and the creator of the associated main object can mark the comment as completed
None of these rules hold in the current version: the permission requirements of DomainComment are empty, so no user can create comments, and the update and delete permissions are always false.
# A Real Example in the System
The following uses a real example from the platform to show how to add revision and trash bin support to a Domain object.
# Business Scenario
DynamicLogic is a core asset of the development platform, so we want revision management, trash bin support and comment support for it, to avoid losing any development work, to allow quick recovery when logic goes wrong, and to add related notes. The comment feature is not available in the current version; see Comment Support.
# Domain Definition
DynamicLogic: main objectDynamicLogicRevision: history revision objectDynamicLogicTrash: trash objectHasComment: marks the object as supporting comments
# DynamicLogic
Only properties related to trash bin, multi-revision and comment support are listed here. For other properties, see the definition of DynamicLogic in the platform source code.
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>, // --> Declares revision support
Trashable<DynamicLogic>, // --> Declares trash bin support
HasComment<DynamicLogic>, // --> Declares comment support
tech.muyan.domain.DynamicLogic {
// ....
// --> The revision field is inherited from HasRevision and records the revision number of the current main object
// Comments field, records all comments of the current main object
List<DomainComment> comments
static fetchMode = [
// .....
// Comments, queried using JOIN
comments: org.hibernate.FetchMode.JOIN,
]
static hasMany = [
// .....
// Revisions
revisions: DynamicLogicRevision, // --> List of history revisions
comments : DomainComment, // --> List of comments, one-to-many with the main object
]
static mappedBy = [ // --> Mapping to history revisions
revisions : 'mainObject' // --> The main object reference in history revisions is the mainObject field
]
@Override // --> Returns the sorted list of history revisions, in ascending order of revision number
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() { // --> Returns the list of properties that bump the revision
return ['code'] // --> The revision is bumped only when the code property changes
}
}
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
Only properties related to multi-revision support are listed here. For other properties, see the definition of DynamicLogicRevision in the platform source code.
@ManagedEntity
class DynamicLogicRevision implements // --> Naming convention: main model class name + Revision
DomainRevision<DynamicLogicRevision>, // --> Declares a history revision object
MultiTenant<DynamicLogicRevision>, Auditable {
DynamicLogic mainObject // --> Main object of the history revision
// --> The revision field is inherited from DomainRevision and records the revision number of the history revision
// --> The DomainRevision model must also include all non-foreign-key fields of the main object
}
2
3
4
5
6
7
8
9
10
# DynamicLogicTrash
Only properties related to the trash bin are listed here. For other properties, see the definition of DynamicLogicTrash in the platform source code.
@ManagedEntity
class DynamicLogicTrash implements // --> Naming convention: main model class name + Trash
DomainTrash<DynamicLogicTrash>, // --> Declares a trash object
MultiTenant<DynamicLogicTrash> {
static constraints = {
foreignKeys nullable: true, blank: false // --> The foreign keys field may be null, but not an empty string
}
static mapping = {
// --> Type definition of the foreign keys field: stored as JSON, read into Java as a String
foreignKeys type: 'tech.muyan.rdbms.postgres.JSONBType', sqlType: "jsonb", defaultValue: "'{ }'"
}
Trashable<DynamicLogic> getNullObject() {
// --> When the main object is trashed, foreign key associations pointing to it are set to this NullObject, to avoid foreign key constraint errors
return DynamicLogic.findByName("Dummy Dynamic Logic")
}
static transients = ['nullObject'] // --> The nullObject field (corresponding to the getNullObject method) does not need to be persisted to the database
// --> The DomainTrash model definition must also include all non-foreign-key fields of the main object
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Action Association
The built-in Actions available in the system are:
- Restore a deleted object from the trash bin
- Set a history revision as the latest revision
- Compare the differences between two history revisions
- Compare the differences between a history revision and the current revision
;; File 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,
2
3
4
5
6
# Form Definition
# DynamicForm.csv
;; File 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
2
3
4
5
6
7
8
The last column, accessRequirement.name, is the permission requirement (RoleRequirement name) needed to access the form.
# DynamicFormField_with_displayType.csv
;; File 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
2
3
4
5
6
7
8
9
# Domain Instance Dynamic Field Support
Removed as of 1.0 (DefaultDynamicEntityType / DefaultDynamicEntityInstance and the dynamic field definition and instance models have been deleted; to extend fields for business needs, use the dynamic domain model).
# Domain Design Conventions
For this topic, see Domain Design Conventions.