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 视图。

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

create screens 1

视图创建向导的第一步中,我们选择 Entity list and detail screen(实体列表和详情视图)模板:

screen wizard 1

点击 Next

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

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

create screens 2

这样能确保关联的 User 实体会与 Department 实体一起加载,并在列表视图展示。

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

点击 Next

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

create screens 3

点击 Next

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

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

运行应用程序

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

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

run app 1

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

点击 Save and run

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

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

点击主菜单的 ApplicationDepartments,打开 Department.list 视图:

run app 2

点击 Create,打开 Department.detail 视图:

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 定制

我们对应用程序的 UI 做一些修改,这样你能更加熟悉 Jmix 的功能。

更改属性名称

也许你已经注意到,为 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

切换回浏览器中运行的应用程序。刷新网页,可以看到 hrManager 属性的新名称。

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

只需要在 IDE 中保存修改(按下 Ctrl/Cmd+S)稍等片刻并刷新网页。

DataGrid 中的排序

默认情况下,用户可以对数据网格中展示的数据按照某一列进行排序。这里我们启用按多列排序。

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

customize ui 1

Studio 支持直接在 IDE 中预览页面的布局。点击 Start Preview 按钮:

customize ui 2

Studio 会自动构建前端代码,稍等片刻后,可以在源码旁边的面板内看到页面预览面板。根据你显示器的大小不同,可以选择仅显示 XML 编辑器或者同时显示预览。通过编辑器顶部的按钮切换:

customize ui 3

Jmix UI 的结构面板中,选中 departmentsDataGrid。则同时在预览面板、XML 编辑器以及右下角的 Jmix UI 组件属性面板都会选中该组件:

customize ui 4

勾选 multiSort 复选框:

customize ui 5

Studio 会自动为 dataGrid XML 元素添加 multiSort="true" 属性。

反过来操作也可以。比如,直接在 XML 中编辑,则预览面板和组件属性也能同时反映改动。

切换至运行中的应用程序,并刷新部门列表视图页面。点击 Name 列测试排序功能,然后可以点击 HR Manager 列试试。

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

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

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 时可以节省重启应用的时间。但是对实体的修改是不支持热部署的。