定义模型

Dynamic model → Dynamic model settings 视图提供了编辑动态模型的管理 UI。可以通过可视化编辑器添加实体、属性、枚举和其他元素,然后 Apply 变更,验证通过后发布新的模型版本。

下面的每个小节都展示了编辑器为对应元素生成的 YAML — 与在编辑器的 YAML 视图中直接查看和编辑的内容相同。

关于动态模型的工作原理以及版本如何管理,请参见 概念;关于 YAML 元素的完整定义,请参见 YAML 参考。

使用模型编辑器

打开 Dynamic model settings 视图。Visual 编辑器将当前模型呈现为由实体、属性、枚举、视图组成的连接表结构。可以使用对话框添加和编辑元素。

还可以随时切换到 Code 展现方式,直接检查或编辑原始的 YAML 定义;两种展现方式保持同步。

模型准备好后,点击 Apply。框架会将编辑后的模型与当前激活的模型进行比较,并打开 Dynamic model changes 视图,该视图用于在提交前显示将要应用的新内容。在 Model migrations 下列出每个单独的改动 — 例如创建实体、添加或删除属性、或添加唯一约束。如果有任何视图的变更,会列在 Changed views 下。

检查完列表的改动后,点击 Apply changes 确认。模型将进行验证,如果没问题,则变更生效并发布为新的模型版本。如果直接关闭 Dynamic model changes 视图,将丢弃待处理的变更,系统中激活的模型保持不变。

属性顺序

Jmix 3.1+

在 Attributes 标签页,选择一个属性,然后使用 Move the attribute up 和 Move the attribute down 按钮更改其位置。属性的顺序保存在模型中。会按照这个顺序生成详情视图中字段的顺序、列表视图中列的顺序,以及元数据中属性的顺序,REST API、实体探查和通用过滤器也会使用该顺序。新顺序在发布模型时生效。

只能移动动态属性。静态实体自身的属性和动态实体的标识符位置不变。数据库中列的顺序也不会改变。

模型历史

Jmix 3.1+

在 Code 展示中,点击 History 查看保存的模型所有版本,最新的在前面。对于每个版本,对话框显示日期、发布的用户以及是否为激活版本。选择一个版本可以查看 YAML,然后点击 Copy to clipboard 复制。

该对话框不会发布较早的模型。如需返回到较早版本,请复制其 YAML,然后粘贴到 Code 展示中,然后发布模型。

扩展静态实体

如需为应用程序中已存在的实体(例如 Customer 这样的静态实体)添加动态属性,在编辑器中选择该实体并向其添加属性。动态属性与实体自身的表分开存储,并在运行时与静态属性一样可用。

下面的示例中,为 Customer 添加了一个 taxId 字符串属性,长度为 20,带有必填标志、唯一性和验证规则:

- name: "Customer"
  attributes:
    - name: "taxId"
      javaClass: "java.lang.String"
      length: 20
      required: true
      unique: true
      validation:
        constraints:
          - annotation: "Pattern"
            parameters:
              regexp: "^[A-Z0-9-]+$"
            message:
              en: "Tax ID can contain uppercase letters, digits and dashes only"
      messages:
        en: "Tax ID"

定义动态实体

动态实体是一个全新的实体,仅在运行时存在 — 即,在源代码中没有 Java 类或表。创建时,请在编辑器中添加一个与已有实体名称不同的新实体,然后定义其属性,并定义其视图(可选)。有关如何为动态实体生成视图和菜单项,请参见 视图和菜单项。

下面的示例定义了 LoyaltyLevel 动态实体,包含多个属性、验证约束以及几个生成的视图:

- name: "LoyaltyLevel"
  resourceRoles:
    read:
      - "employee"
      - "manager"
    create:
      - "manager"
    update:
      - "manager"
    delete:
      - "manager"
  messages:
    en: "Loyalty level"
    de: "Treuestufe"
  validation:
    constraints:
      - name: "discountDescriptionRequired"
        target: "entity"
        type: "expression"
        evaluator: "spel"
        expression: "discount == null || description != null"
        attributes:
          - "discount"
          - "description"
        path: "description"
        groups:
          - "UiCrossFieldChecks"
          - "RestApiChecks"
        message:
          en: "Description is required when discount is specified"
  attributes:
    - name: "name"
      javaClass: "java.lang.String"
      instanceName: true
      messages:
        en: "Name"
    - name: "description"
      javaClass: "java.lang.String"
      lob: true
      messages:
        en: "Description"
    - name: "publicSummary"
      javaClass: "java.lang.String"
      calculated:
        evaluator: "spel"
        expression: "(name ?: '') + ': ' + (description ?: '')"
        dependsOn:
          - "name"
          - "description"
      messages:
        en: "Public summary"
    - name: "discount"
      javaClass: "java.math.BigDecimal"
      validation:
        constraints:
          - annotation: "DecimalMin"
            parameters:
              value: "0"
          - annotation: "DecimalMax"
            parameters:
              value: "100"
      messages:
        en: "Discount"
  views:
    - type: "list"
      viewId: "LoyaltyLevel.list"
      viewRoute: "loyalty-levels"
      viewTitle:
        en: "Loyalty levels"
        de: "Treuestufen"
      templateParams:
        includeProperties: ["name", "discount"]
        excludeProperties: ["publicSummary"]
      resourceRoles:
        - "employee"
        - "manager"
      menuItem:
        parentMenu: "application"
        insertBefore: "Customer.list"
        icon: "vaadin:star"
        title:
          en: "Loyalty levels"
        resourceRoles:
          - "employee"
          - "manager"
      descriptor:
        template: "default"
    - type: "detail"
      viewId: "LoyaltyLevel.detail"
      viewTitle:
        en: "Loyalty level"
      descriptor:
        template: "default"
      handlers:
        - event: "initEntity"
          expression: "entity.name = 'New level'"
        - event: "valueChange"
          componentId: "discountField"
          expression: "components.setRequired('descriptionField', event.value != null)"
    - type: "read"
      viewId: "LoyaltyLevel.read"
      viewTitle:
        en: "Loyalty level"

属性

属性的不同类型由其定义的字段区分:数据类型属性、引用属性、集合属性或枚举属性。

数据类型属性

数据类型属性保存一个简单值,例如字符串、数字、日期或布尔值。通过选择类型进行定义,该类型存储为 javaClass。对于字符串和字节数组属性,还可以设置 length 并使用 lob 标记为对象存储。扩展静态实体 中显示的 taxId 属性就是一个数据类型属性。

有关支持类型的完整列表,请参见 支持的类型。

文件引用属性

Jmix 3.1+

FileRef 类型的属性保存 文件存储 中文件的引用。静态实体的动态属性和动态实体的属性都可以设置为此类型。值存储在字符串字段中。

在属性对话框中,选择 FileRef 类型。此类型下 Length 和 Lob 字段会被隐藏,因此该列的默认长度为 1024 个字符。在 YAML 中,javaClass 为 io.jmix.core.FileRef。

生成的视图以及从 模板 创建的 XML,都会使用 fileStorageUploadField 作为此类属性的 UI 组件。

实例名称

为属性勾选 Instance name,以将其值用作实体的 实例名称。一个实体只能有一个实例名称属性。如果另一个属性已经有了该标志,属性对话框会显示其名称,选择该标志会将其移动到当前属性。计算属性也可以作为实例名称。

引用属性

引用属性是关联另一个实体的单值链接属性。选择目标实体,在运行时该属性存储为关联实体实例的引用。

- name: "loyaltyLevel"
  entityName: "LoyaltyLevel"
  lookup:
    type: "DROPDOWN"
    actions:
      - "entity_lookup"
      - "entity_clear"
  messages:
    en: "Loyalty level"

查找字段

Jmix 3.1+

对于单值引用属性,可以选择使用特定的组件在生成的视图中选择实体实例。在属性对话框的 Lookup field type 和 Lookup actions 字段中进行设置。上面示例中的 loyaltyLevel 属性使用下拉列表。

Lookup field type 可以有以下值:

  • DROPDOWN – 一个 entityComboBox。条目在用户输入时按引用实体的实例名称加载,因此该类型适用于大型表。如果被引用实体的实例名称不基于字符串属性,则该控件改用查找视图。

  • VIEW – 一个 entityPicker,打开查找视图。如果未设置操作,则该控件使用标准查找和清除操作。

如果未设置类型,则使用默认组件。

Lookup actions 设置控件的按钮:entity_lookup、entity_open、entity_clear。

这些设置的作用类似于常规实体引用属性上的 @LookupField 注解。

集合属性

集合属性保存子实例的有序列表(组合)。目标实体必须是动态实体。在编辑器中将属性标记为集合,保存多个实体引用的有序值:

- name: "benefits"
  entityName: "Benefit"
  collection: true
  messages:
    en: "Benefits"

枚举属性

枚举属性存储动态枚举的一个值。选择该属性所使用的枚举:

- name: "grade"
  enumeration: "CustomerGrade" # short name, resolved via base package
  messages:
    en: "Grade"

枚举

动态枚举定义了一组固定的命名值,可由枚举属性使用。在编辑器中添加枚举,为其命名,并使用 id 和名称定义值。

enumerations:
  - name: "CustomerGrade"
    messages:
      en: "Customer grade"
    values:
      - name: "PLATINUM"
        id: "30"
        messages:
          en: "Platinum"
      - name: "GOLD"
        id: "20"
        messages:
          en: "Gold"
      - name: "BRONZE"
        id: "10"
        messages:
          en: "Bronze"

一个枚举中的所有值 id 必须是同一种类型:要么全部是整型 id(如上例所示),要么全是字符串 id。

计算型属性

计算型属性是只读的,在访问时根据表达式计算而非存储。使用 Spring Expression Language (SpEL) 编写表达式,并列出该属性 dependsOn 的属性,以便共同加载。

- name: "publicSummary"
  javaClass: "java.lang.String"
  calculated:
    evaluator: "spel"
    expression: "(name ?: '') + ': ' + (description ?: '')"
    dependsOn:
      - "name"
      - "description"
  messages:
    en: "Public summary"

由于计算属性的值不存储在数据库中,计算型属性不能用于数据库级别的过滤或排序。

验证

可以为属性添加验证约束以限制其值的范围。每个约束都有一个类型(例如 Pattern、Size 或 DecimalMax)、参数(可选)和消息(可选)。

validation:
  constraints:
    - annotation: "Pattern"
      parameters:
        regexp: "^[A-Z0-9-]+$"
      message:
        en: "Tax ID can contain uppercase letters, digits and dashes only"

还支持实体级别的验证,实体级别的约束可以跨越多个属性。有关约束的完整列表和实体级别验证,请参见 YAML 参考。

唯一约束

如果要求属性值在所有实例中唯一,请将其标记为唯一。这是单属性唯一约束的简写形式:

unique: true

如果要求多个属性的组合唯一,需要在实体上定义复合唯一约束:

uniqueConstraints:
  - name: "customerCountryTaxIdUnique"
    attributes:
      - "countryCode"
      - "taxId"
    message:
      en: "Tax ID must be unique within a country"

唯一性由数据库约束强制执行。对于可软删除的静态实体,不支持唯一约束。

安全

可以使用资源角色限制对动态实体和属性的访问。授权是累加性质的 — 列出某个角色即授予其相应的访问权限,未授予的权限将被拒绝。使用的角色必须已存在于应用程序中;下面的示例使用了 employee 和 manager 角色。

在实体级别,授予 read、create、update 和 delete 访问权限。动态实体支持此功能:

resourceRoles:
  read:
    - "employee"
    - "manager"
  create:
    - "manager"
  update:
    - "manager"
  delete:
    - "manager"

在属性级别,授予 view 和 modify 访问权限:

- name: "countryCode"
  javaClass: "java.lang.String"
  length: 2
  resourceRoles:
    view:
      - "employee"
      - "manager"
    modify:
      - "manager"
  messages:
    en: "Country code"

有关资源角色的更多信息,请参见 资源角色。

继承角色

Jmix 3.1+

为某个角色授予的访问权限也会在该角色的子角色中生效(无论层级多深),因此,有子角色的用户也会拥有父角色的权限。例如,如果模型授予 employee 角色读取权限,而 employee 是 manager 的父角色,那么仅拥有 manager 角色的用户会获得此权限。这个规则对以下类型的角色有效:

  • 运行时创建的,有 父角色 的角色。

  • 设计时创建的,扩展其他角色接口的角色接口。

同样的规则适用于模型中的所有 resourceRoles 角色的分配,包括对视图和菜单项的权限,以及 jmix.dynmodel.security.full-access-roles 属性中列出的角色。

角色作用域 仅针对分配给用户的角色进行检查。父角色的作用域不会检查。

模型配置的访问权限

扩展组件为 Dynamic model settings 视图提供以下资源角色。可以为需要配置模型的用户分配所需的角色。

角色名称 代码 访问权限

Dynamic model: view settings

dynmod-view-settings

打开模型设置和模型历史。不能更改模型。

Dynamic model: modify settings

dynmod-modify-settings

完全访问权限。可以做任何更改,包括删除数据。

Dynamic model: modify settings without data loss

dynmod-safe-modify-settings

仅支持不会丢失数据的更改。

不丢失数据的更改

Jmix 3.1+

"Dynamic model: modify settings without data loss" 角色允许用户扩展模型而不会产生丢失数据的风险。有此角色的用户可以在 Visual 和 Code 展示中编辑模型,并发布以下更改:新实体、属性、枚举和枚举值,以及对验证、视图、菜单项、消息和资源角色的更改。

用户不能发布可能丢失数据的更改:

  • 移除已发布的实体或属性;

  • 在 YAML 中重命名已发布的实体或属性,因为重命名会发布为移除和创建两个动作;

  • 移除已发布的枚举;

  • 从已发布的枚举中移除值。

在 Visual 展示中,已发布的内容不能被移除。在最后一次发布后添加的内容仍然可以被移除。如果编辑后的模型包含不允许的改动,Dynamic model changes 视图会列出这些改动,并且在移除这些改动之前无法发布模型。

该限制在服务器端检查。因此无法通过以另一种方式保存模型来规避,例如通过实体检查。