# 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:

Domain model list

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
1
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,,,
1
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

Auto-generated model names

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"
}
1
2
3
4
5
6
7
8
9
10
  • BIG_DECIMAL: scale is the number of decimal places and precision is the precision (integer digits + decimal digits). A decimal(precision, scale) column is created only when both are set and neither is 0; when only one of them is set, or scale is set to 0, the column is created as an unlimited-precision decimal (so use INTEGER or LONG when you need integers).
  • ENUM / ENUM_LIST: enumClass is 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: mappedBy is 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:

  1. labelField: which property of the object is displayed as its identifier in frontend object controls; when not set, id is displayed.
  2. inlineSearchColumns: which properties are searched when quickly searching for objects in frontend object input controls.
  3. loadAfter: when importing seed data, after which types of objects the objects of this type must be imported. For details, see Import Order.
  4. 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.
  5. objectCloneLogicName: the name of the dynamic logic (logic type OBJECT_CLONE_CORE_LOGIC) used when copying objects of this model; when not set, the system's built-in Default Clone Object Core Logic is 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"
}
1
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_OBJECT or MAPPED_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: "'{ }'"
  }
  // ....
}
1
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.

  1. The name field 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. The name field is usually designed to be non-updatable.
  2. The label field is usually used to display a summary of the object in the UI's Object display control.
  3. Both the name and label fields are usually added to the Domain's inlineSearchColumns property.
  4. The label field is usually set as the Domain's labelField.
  5. The description field 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:

  1. DynamicAction (object action): logic type DYNAMIC_ACTION_ENABLE_LOGIC, determines whether the action is available
  2. DynamicDashboardWidget (dashboard widget): logic type DASHBOARD_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: the objectType field is usually used to record the type of the object; it is a foreign key reference to a tech.muyan.DomainClass object.

  • objectId(s): the objectId(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 objectType and objectIds fields in tech.muyan.message.Message record the type and ids of the objects associated with the message.
  • The objectType and objectId fields in tech.muyan.comment.DomainComment record the type and id of the object associated with the comment.
  • The objectType field in tech.muyan.dynamic.hook.DynamicObjectHook records 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.

Last Updated: 9/24/2026, 2:27:35 PM