3. 关联和唯一属性

我们的入职应用程序的下一个需要完成的功能是部门的管理。

入职的新员工要分配给某个部门。每个部门有唯一的名称并关联一位 HR 经理。

在本节中,我们将完成:

  • 关联至 UserDepartment 实体。

  • 带有外键和唯一约束的数据库表。

  • 包含选择关联实体 UI 组件的 CRUD 界面。

创建 Department 实体

Department 实体具有 name(唯一)属性和关联至 User 实体的 hrManager 属性:

references diagram

如果你的应用程序正在运行,先通过主工具栏的 Stopsuspend)按钮停止运行。

前一节 一样,在 Jmix 工具窗口中,点击 Newadd)→ JPA Entity。然后在 New JPA Entity 对话框中,Class 字段输入 Department 并选中 TraitsVersioned 复选框:

create entity 1

点击 OK

Studio 将创建实体类并打开实体设计器:

create entity 2

创建唯一属性

我们为实体添加 name 属性。

Attributes 工具栏中点击 Addadd)。然后在 Name 字段输入 name,并选中 Mandatory 复选框:

create entity 3

现在我们为数据库的 NAME 列定义唯一约束。目的是保证部门不会重名。

从底部切换至 Indexes 标签页,在 Database Indexes 工具栏中点击 New Indexadd)。Studio 会在索引列表中新加一行:

create entity 4

Available attributes 列表中选择 NAME 并点击工具栏中的 arrow right 按钮将其移至 Selected attributes 列表。

在索引行勾选 UniqueConstraint 复选框,并修改索引名称为 IDX_DEPARTMENT_UNQ_NAME

create entity 5

如果切换回实体设计器的 Text 标签页,可以看到唯一约束已经添加至 @Table 注解中了:

@JmixEntity
@Table(name = "DEPARTMENT", uniqueConstraints = {
        @UniqueConstraint(name = "IDX_DEPARTMENT_UNQ_NAME", columnNames = {"NAME"})
})
@Entity
public class Department {
    // ...
}

使用这个注解,Studio 会在 Liquibase 更改日志中为该表添加一个唯一约束。

创建关联属性

现在我们创建部门实体中关联至 User 实体的 HR 经理属性。

点击 Attributes 工具栏的 Addadd),然后在 New Attribute 对话框中,Name 字段输入 hrManager,然后选择:

  • Attribute typeASSOCIATION

  • TypeUser

  • CardinalityMany to One

create entity 6

点击 OK

可以在 Text 标签页查看该新建属性的源码:

@JoinColumn(name = "HR_MANAGER_ID")
@ManyToOne(fetch = FetchType.LAZY)
private User hrManager;

同时,类的 @Table 注解中也定义了这个外键的索引:

@JmixEntity
@Table(name = "DEPARTMENT", indexes = {
        @Index(name = "IDX_DEPARTMENT_HR_MANAGER", columnList = "HR_MANAGER_ID")
    },
    // ...
)

Indexes 标签页也能看到这个索引。

创建 CRUD 界面

现在我们为 Department 实体生成 CRUD 界面。

在实体设计器顶部的操作面板中,点击 ScreensCreate screen

create screens 1

界面创建向导的第一步中,我们选择 Entity browser and editor screen(实体浏览和编辑界面)模板:

screen wizard 1

点击 Next

向导的后两步中,我们都使用默认推荐的设置。

Entity browser fetch plan 步骤中,选择添加 hrManager 属性:

create screens 2

这样能确保关联的 User 实体会与 Department 实体一起加载,并在浏览界面展示。

如果某个属性不在 fetch plan 展示,在生成的界面中,Studio 不会为该字段创建可视化组件。

点击 Next

Entity editor fetch plan 步骤中,会自动选择该属性:

create screens 3

点击 Next

Localizable messages 步骤使用默认的配置,点击 Create

Studio 会生成两个界面:Department.browseDepartment.edit,并打开其源码。可以暂时关闭所有的代码编辑器,本节后面部分会对生成的界面做一些修改。

运行应用程序

点击主工具栏中的 Debugstart debugger)按钮启动应用程序。

在运行应用程序之前,Studio 会生成 Liquibase 更改日志:

run app 1

可以看到,更改日志中的语句创建了 DEPARTMENT 表、NAME 列的唯一约束和外键,以及 HR_MANAGER_ID 列的索引。

点击 Save and run

Studio 会执行更改日志,然后构建并运行应用程序。

应用程序准备好后,在浏览器打开 http://localhost:8080 并使用 admin / admin 凭证登录。

点击主菜单的 ApplicationDepartments,打开 Department.browse 界面:

run app 2

点击 Create,打开 Department.edit 界面:

run app 3

可以点击选择控件中的省略号按钮为部门选择一个 HR 经理。点击后会在当前部门编辑界面之上打开用户浏览界面,界面的面包屑会显示当前界面的结构。当在用户表中选定一行后,Select 按钮会变成激活状态:

run app 4

选择一个用户并点击 Select,选中的用户会显示在选择控件中:

run app 5

点击 OK。关联的用户实体也会在表格中展示:

run app 6

实例名称

你可能会好奇为什么选择控件和表格会显示 [admin] 呢?

Jmix 有一个概念叫做 实例名称(instance name),以一种易读的方式表示一个实体实例。可以通过在实体属性或方法上添加 @InstanceName 注解进行定义。

项目模板生成的 User 实体有下面的方法定义实例名称:

public class User implements JmixUserDetails, HasTimeZone {
    // ...

    @InstanceName
    @DependsOnProperties({"firstName", "lastName", "username"})
    public String getDisplayName() {
        return String.format("%s %s [%s]", (firstName != null ? firstName : ""),
                (lastName != null ? lastName : ""), username).trim();
    }
}

因此,当 firstNamelastName 都为空时,User 的实例名称显示为方括号中的 username,也就是上面我们看到的。

如果实体中有合适的属性时,比如 namedescription 等,Studio 的实体设计器会自动生成 @InstanceName 注解。在我们的例子中,Department 实体的 @InstanceName 注解就放在了 name 属性上:

public class Department {
    // ...

    @InstanceName
    @Column(name = "NAME", nullable = false)
    @NotNull
    private String name;
}

这样一来,如果其他实体中有关联 Department 实体的话,UI 中就会显示部门的名称。本教程后面会有这种情况。

实体设计器也支持手动定义实例名称。支持通过 Instance name 字段选择某个属性或点击按钮生成返回实例名的方法:

instance name 1

简单的 UI 定制

自动生成的部门 CRUD UI 看上去还可以接受,但是有些细节还是需要调整一下。

更改属性名称

也许你已经注意到,为 hrManager 属性自动生成的名称不是很对,生成的是 Hr manager,我们希望改成 HR Manager

在实体设计器中选中 hrManager 属性,然后点击属性名称旁边的地球仪(globe)按钮:

change caption 1

会显示 Localized Message 弹窗,如果是多语言环境,比如添加了中文支持,那么这里还会显示一格中文的文本框:

change caption 2

这里我们先修改内容为 HR Manager,并点击 OK

如果在 Jmix 工具窗口中双击 User InterfaceMessage Bundle 节点,可以修改整个项目的本地化消息。我们刚才修改的内容如下:

change caption 3

切换回浏览器中运行的应用程序。关闭部门的 CRUD 界面并再次打开。可以看到 hrManager 属性的新名称。

由于 Studio 带有 热部署 功能,无需重启应用程序即可看到 UI 的改动。

只需要在 IDE 中保存修改(按下 Ctrl/Cmd+S)稍等片刻并重新打开界面,就可以查看新的改动。

注意,刷新浏览器网页并不会更新 UI,因为 UI 状态是在服务端保存的。重新打开界面需要在应用程序中关闭页面的标签页,然后再次从主菜单或者其他界面打开。

自定义实体选择器的操作

默认情况下,当点击 HR 经理选择控件的省略号按钮时,新弹出的用户选择界面会完全覆盖部门的编辑界面。这里我们改成通过弹出对话框窗口的形式展示用户选择界面。

Jmix 工具窗口找到 department-edit.xml 文件并双击打开。然后会显示界面设计器:

customize ui 1

根据显示器的分辨率不同,你可能只想看源码或者只想看界面预览,这可以通过编辑器顶部的按钮切换:

customize ui 2

切换至 Editor and Preview 模式,在 Jmix UI 层级面板中,点击 hrManagerField。选择后,在预览图、XML 编辑器和右下方的 Jmix UI 组件面板中,都会同时展示该组件:

customize ui 3

可以看到 entityPicker 元素有一个内部的 actions 元素,带有两个操作。每个操作分别对应于选择控件上的两个按钮:entityLookup 操作展示用于选择关联实体的界面,entityClear 操作清除当前控件选择的值。

通过设置不同的属性参数可以对操作进行定制化修改。

Jmix UI 层级面板中选择 entityLookup 操作,然后在组件面板中 openMode 属性的下拉列表中选择 DIALOG 值:

customize ui 4

修改也同样会反映在 XML 中。

这种同步修改的机制反过来也可以。直接编辑 XML 后,改动会同步至设计器面板和预览界面中。

切换至运行中的程序。选择一个部门,然后点击 Edit 按钮打开部门编辑界面。在 HR 经理选择控件中点击省略号按钮。

现在选择用户的界面是以可移动的弹窗方式展示:

customize ui 5

更改违反唯一约束通知消息

如果尝试创建另一个同名的部门,则可以看到违反唯一约束的错误消息:

customize ui 8

默认的消息不是特别友好,可以进行定制化修改。

Jmix 工具窗口双击 User InterfaceMessage Bundle 节点,并添加下面这一行内容:

databaseUniqueConstraintViolation.IDX_DEPARTMENT_UNQ_NAME=A department with the same name already exists

消息的键值需要以 databaseUniqueConstraintViolation. 开头,并带上数据库唯一约束的名称。你也许注意到,在该文件内已经存在类似的消息,是配置给 User 实体的 username 属性的。

切换至应用程序并测试我们的改动。现在错误消息显示好一些了:

customize ui 9

小结

本节中,我们构建了第二个功能:部门的管理。

学习内容:

  • Studio 帮助创建关联属性并生成带有外键和索引的 Liquibase 变更日志

  • 为了在浏览或编辑界面展示关联属性,需要将属性包含在界面的 fetch plan 中。

  • 实例名称 用来在 UI 中展示关联实体。

  • 在自动生成的编辑界面中,默认使用 EntityPicker 实体选择器 组件选择关联实体。组件的 操作 支持定制化修改,比如在对话框中展示选择界面。

  • 实体属性的唯一性 是在数据库级别通过定义唯一约束进行维护的。

  • 违反唯一约束的错误消息可以轻松实现 自定义

  • Studio 生成的标题和消息都保存在应用程序的 消息包 中。

  • Studio 可以 热部署 对界面和消息的改动,在开发 UI 时可以节省重启应用的时间。但是对实体的修改是不支持热部署的。