# Dynamic Domain Model
Screenshots on this page are taken from the Chinese UI. Menu, field and button names in the text use the English UI labels.
This system uses a self-developed ORM framework based on JDBC that supports defining domain models dynamically at runtime (dynamic models, of type DYNAMIC): once a definition is saved, the system automatically creates the table and adds columns, and provides the model with the same create, read, update, delete, import/export and permission control capabilities as GORM domain models.
# Target Audience
This document is intended for implementers and developers of this system
The following describes development notes on features supported by domain models, currently including
- How dynamic models are created and naming rules
- Domain model field property settings
- Domain model extended metadata configuration
- Indexes and model extension
# Viewing Domain Models
All domain models in the system (including the platform's built-in GORM models and dynamic models) are listed in Development > Classes. You can search by short name, full name or database table name, and view or modify a model definition and its fields via Details and Update:

Models cannot be created in the UI in the current version
The Development > Classes list has no Create button: in the seed data, the create permission requirement (createRoleRequirement) of DomainClass is empty, and when a create permission requirement is empty, no user has create permission (the API GET /permissions/DomainClass/create returns {"create": false}).
Therefore new dynamic models must be imported via seed data CSV, or imported together with the seed data of a dynamic plugin. Fields of existing models can be adjusted in Update.
UI issues in the update dialog
In the current version, when the Update dialog of a dynamic model is opened, the "Options" column of every row in the "Dynamic class fields" sub-table displays Unsupported displayType: tag_list, and an extra "0" appears at the end of each row. These are UI issues in the current version and do not affect the display of the other columns in the sub-table; the options of a field can be set via the options column of the seed data CSV.
# Creating Dynamic Models via Seed Data
A dynamic model definition is imported from two CSV files placed in the seed data directory, with file names starting with the domain model name (for example DomainClass_example.csv, DomainClassField_example.csv). For import rules, see Data Import.
Model definition (DomainClass): only the short name needs to be filled in; the other names are generated automatically by the system.
shortName(*),extInfo,createRoleRequirement.name,readRoleRequirement.name,updateRoleRequirement.name,deleteRoleRequirement.name
SampleDynamicOrderDomain,,DEVELOPER,DEVELOPER,DEVELOPER,DEVELOPER
2
The last four columns are the model-level create, read, update and delete permission requirements (RoleRequirement names). When a permission requirement is empty, no user has that permission, so for a model that is to be used in the UI, all four should be configured.
Field definition (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,,,
2
3
4
5
6
7
8
9
# Naming Rules and Auto-generated Names
Both the model's short name (shortName) and the field name (name) must match the regular expression ^[a-zA-Z][a-zA-Z0-9]*$: start with a letter and contain only letters and digits, no underscores, spaces or Chinese characters. Otherwise, creating the model reports InvalidDomainName (error code 14002), and creating the field reports InvalidDomainFieldName (error code 14003).
When a model is created, the system automatically generates the following information from the short name; it cannot be modified after creation:
| Information | Generation rule | Example (short name SampleDynamicOrderDomain) |
|---|---|---|
| Full name (fullName) | DYNAMIC-<short name> | DYNAMIC-SampleDynamicOrderDomain |
| Table name (tableName) | <tenant>_<short name converted to lower snake case> | muyan_sample_dynamic_order_domain |
| Label (label) | Camel case split into space-separated words | Sample Dynamic Order Domain |

A field's database column name is the field name converted to lower snake case (for example orderDate â order_date); if the result is a PostgreSQL keyword (such as user, order, group), the suffix _col is added; column names are at most 63 characters long.
# Domain Model Field Property Settings
# Field Property Settings
| Field | Description |
|---|---|
| Name (name) | Name of the field, used to reference the field in code. Field names must be unique within the same domain model. Cannot be modified after creation |
| Data Type (dataType) | Data type of the field; see below for details. Cannot be modified after creation |
| Reference Domain (referenceDomain) | Required when the data type is DOMAIN_OBJECT or DOMAIN_OBJECT_LIST; specifies the associated domain model. FILE / FILE_LIST types are automatically associated with StorageFieldValue. Cannot be modified after creation |
| Nullable (nullable) | Whether the field can be empty; corresponds to the NOT NULL constraint of the database column |
| Editable (editable) | Whether the field value can be modified after the object is created |
| Default Value (defaultValue) | Default value of the field; corresponds to the DEFAULT of the database column |
| Options (options) | List of allowed values for the field, as a JSON array string in the format ["option1", "option2"]. Once set, a CHECK constraint named <column name>_options is created in the database, allowing only these values to be saved |
| Extend information (extInfo) | Other additional configuration; different data types may have different extended information, see below |
| Comment (comment) | Field comment, written to the database column comment (COMMENT ON COLUMN) |
# Data Type Details
# Field Data Types
The field data types (the FieldDataType enum) supported by the system include:
| Data type | Description |
|---|---|
| STRING | String |
| BOOLEAN | Boolean (true or false) |
| BIG_DECIMAL | High-precision decimal number; precision and scale can be set in extInfo |
| INTEGER | Integer |
| LONG | Long integer |
| DOUBLE | Double-precision floating-point number |
| LOCAL_DATE | Date without time and time zone information |
| ZONED_DATETIME | Date and time with time zone information |
| OFFSET_DATETIME | Date and time with time zone offset |
| JSON_STRING | JSON string |
| DOMAIN_OBJECT | Reference to another domain object (many-to-one); the reference domain must be set |
| DOMAIN_OBJECT_LIST | List of domain objects (many-to-many); the reference domain must be set |
| ENUM | Enum; the Java enum class must be specified via enumClass in extInfo |
| ENUM_LIST | Multi-select enum, stored as jsonb; enumClass must also be set in extInfo |
| FILE | Single attachment |
| FILE_LIST | Multiple attachments |
| MAPPED_DOMAIN_OBJECT | One-to-one reverse association field. For example, if class A has a field b that is an object of class B, you can declare a MAPPED_DOMAIN_OBJECT field a in B with referenceDomain set to A, and fill in b in mappedBy of extInfo, meaning the reverse reference is associated with field b of class A. This is a virtual field; no column is created in the table |
| MAPPED_DOMAIN_OBJECT_COLLECTION | Similar to MAPPED_DOMAIN_OBJECT, but for a one-to-many reverse association |
| GENERIC_OBJECT | Reference to a domain object of any type, stored in the form <short name>:<id> |
| GENERIC_OBJECTS | List of references to domain objects of any type, stored as jsonb |
# Extended Information (Ext Info) Field
Fields of different data types can be configured with different content in the extended information, in JSON format:
{
// For Decimal fields: set the number of decimal places (scale) to 2
"scale": 2,
// For Decimal fields: set the precision to 10
"precision": 10,
// Specifies the Java enum type of an ENUM / ENUM_LIST field
// The enum class can be defined in a dynamic plugin
"enumClass": "tech.muyan.mes.enums.WorkTaskStatusEnum",
// For MAPPED_DOMAIN_OBJECT / MAPPED_DOMAIN_OBJECT_COLLECTION fields, the reverse reference field must be set
"mappedBy": "b"
} 2
3
4
5
6
7
8
9
10
BIG_DECIMAL:scaleis the number of decimal places andprecisionis the precision (integer digits + decimal digits). Adecimal(precision, scale)column is created only when both are set and neither is 0; when only one of them is set, orscaleis set to0, the column is created as an unlimited-precisiondecimal(so useINTEGERorLONGwhen you need integers).ENUM/ENUM_LIST:enumClassis the fully qualified class name of the Java enum class; the class can be placed in a dynamic plugin.MAPPED_DOMAIN_OBJECT/MAPPED_DOMAIN_OBJECT_COLLECTION:mappedByis the name of the reverse-referenced field and must be set.
# Domain Model Metadata
Extended domain model metadata is configured in the model's extInfo:
labelField: which property of the object is displayed as its identifier in frontend object controls; when not set,idis displayed.inlineSearchColumns: which properties are searched when quickly searching for objects in frontend object input controls.loadAfter: when importing seed data, after which types of objects the objects of this type must be imported. For details, see Import Order.queryField: during CSV import, if a column associated with this model does not specify a query field in its column name, this field is used to look up the associated object.objectCloneLogicName: the name of the dynamic logic (logic typeOBJECT_CLONE_CORE_LOGIC) used when copying objects of this model; when not set, the system's built-inDefault Clone Object Core Logicis used.
An example:
{
// Quick search by the user matches against the name and label fields
"inlineSearchColumns": ["name", "label"],
// When an Object control is shown in the UI, the value of its label field is displayed as the identifier
"labelField": "label",
// When importing CSV data, the current Domain is loaded after DynamicLogic and User
"loadAfter": ["DynamicLogic", "User"],
// When a CSV column that references the current Domain does not specify a query field, records are looked up by the name field
"queryField": "name"
} 2
3
4
5
6
7
8
9
TIP
In the current version, extInfo can still parse the two settings dynamicTemplate and dynamicEntityInstance (custom entity templates), but no feature in the platform reads them, so they have no effect when configured.
# Indexes
Indexes of dynamic models are defined through the built-in dynamic model DomainClassIndex, whose fields are as follows:
| Field | Description |
|---|---|
name | Index name |
domainClass | The domain model the index belongs to |
fields | Names of the fields in the index, as a JSON array, for example ["orderId", "orderDate"] |
unique | Whether it is unique |
When a DomainClassIndex object is created, the system creates an index named <table name>_<index name in lower case> on the model's data table; when unique is true, a UNIQUE constraint with the same name is created. An empty fields reports IndexFieldsCouldNotBeEmpty (error code 14004), and a field name that does not exist in the model also reports an error.
There is no menu entry for DomainClassIndex; it must be created via seed data or the API, which requires DEVELOPER permission.
TIP
Deleting a DomainClassIndex object does not delete the index or constraint already created in the database.
# Model Extension
A dynamic model can declare that it extends another model through ExtendedDomainClass (fields parent, child): the child model inherits all fields of the parent model and uses them merged with its own fields.
ExtendedDomainClass has no menu entry, and its four permission requirements are all empty, so it cannot be created in the UI or via the API. It can only be imported via seed data (a CSV file whose name starts with ExtendedDomainClass); the system imports it after DomainClass and DomainClassField.
The constraints are as follows:
- Fields with the same name in the parent and child models must have the same data type, otherwise an error is reported;
- Circular extension is not allowed (A extends B, and B extends A);
- If the parent model has fields of type
MAPPED_DOMAIN_OBJECTorMAPPED_DOMAIN_OBJECT_COLLECTION(virtual fields) and the child model has no field with the same name and type, the parent model cannot be extended.
# Output of Collection Fields in the API
The list and detail APIs of dynamic models output all fields, including collection fields such as DOMAIN_OBJECT_LIST. Associated objects are rendered level by level; at the deepest level only the associated object's id and labelField field are output, so there is no infinite recursion.
# Domain Design Conventions
# extInfo Convention
Use the extInfo field to store information that is structurally complex, heterogeneous, or specific to a particular subtype of the Domain, but that still needs to be stored in a structured way.
A sample definition of this field is as follows
// Example of the extInfo field as actually implemented in the platform
class DynamicFormField implements MultiTenant<DynamicFormField>,
Auditable, Serializable {
// ....
// The Java field type is String
String extInfo
// Nullable, but must not be blank
static constraints = {
extInfo nullable: true, blank: false
}
static mapping = {
// Uses a jsonb database column: the database adds the relevant validation automatically and provides JSON query functions
// When converted to a Java object, it becomes a String
extInfo type: 'tech.muyan.rdbms.postgres.JSONBType', sqlType: "jsonb", defaultValue: "'{ }'"
}
// ....
} 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# displaySequence Convention
The displaySequence field is usually used to specify the display order of fields, field groups, tree nodes, DynamicAction and so on in the frontend
# name / label / description Convention
Domains are often designed with a non-updatable name field and an updatable label field.
- The
namefield is usually used to specify foreign key associations with other objects in CSV files; for how to specify foreign key associations in CSV files, see the Associated Object Queries section. Thenamefield is usually designed to be non-updatable. - The
labelfield is usually used to display a summary of the object in the UI's Object display control. - Both the
nameandlabelfields are usually added to the Domain'sinlineSearchColumnsproperty. - The
labelfield is usually set as the Domain'slabelField. - The
descriptionfield is usually used to store business descriptions, help information and so on.
# enableLogic Convention
A field named enableLogic on an object points to a DynamicLogic that determines whether the object is displayed or available. Currently, only the following have an enableLogic field:
DynamicAction(object action): logic typeDYNAMIC_ACTION_ENABLE_LOGIC, determines whether the action is availableDynamicDashboardWidget(dashboard widget): logic typeDASHBOARD_WIDGET_ENABLE_LOGIC, determines whether the widget is displayed
Scheduled tasks (DynamicTask) only have a core logic (coreLogic) and no enable logic; form field groups (DynamicFormGroup) also have no enableLogic field. Although the logic type FORM_GROUP_ENABLE_LOGIC exists (and the seed data contains one dynamic logic of this type), no object currently references it, so it is never executed.
# objectType/objectId(s) Convention
For some specific business scenarios, certain Domains need to record the type and id of their associated objects, usually with a combination of objectType/objectId(s) fields.
objectType: theobjectTypefield is usually used to record the type of the object; it is a foreign key reference to atech.muyan.DomainClassobject.objectId(s): theobjectId(s)field is usually used to record one or more ids of the object; if multiple ids need to be recorded, they are stored as a comma-separated list of ids.
Examples of this convention in the system:
- The
objectTypeandobjectIdsfields intech.muyan.message.Messagerecord the type and ids of the objects associated with the message. - The
objectTypeandobjectIdfields intech.muyan.comment.DomainCommentrecord the type and id of the object associated with the comment. - The
objectTypefield intech.muyan.dynamic.hook.DynamicObjectHookrecords the type of the object associated with the object hook.
# isSystem Convention
Some data is required for the system to run and must not be deleted or modified by users; it is usually identified with a boolean field named isSystem
The system provides a dynamic logic named Has isSystem before Delete (logic type OBJECT_DYNAMIC_HOOK, code file groovy/objectHooks/beforeDeleteObjectWithIsSystem.groovy), used together with a BEFORE_DELETE object hook: before an object is deleted, if isSystem is true, the deletion is not allowed. This check only takes effect in non-development environments; in the development environment (development) objects can still be deleted.
The system has configured this check for DynamicAction, DynamicLogic, DynamicFilter, DynamicPlugin and DynamicConfig.