3. Ссылки и уникальные атрибуты

Следующая часть вашего приложения для онбординга - это управление отделами.

Новый сотрудник принадлежит к Отделу (Department). Каждый отдел имеет уникальное название и ссылку на HR-менеджера.

В этой главе вы создадите:

  • Сущность Department со ссылкой на User.

  • Таблицу базы данных с внешним ключом и уникальным ограничением.

  • CRUD-экраны, включающие UI-компонент для выбора связанной сущности.

Создание сущности Department

Сущность Department имеет уникальный атрибут name и атрибут hrManager, который является ссылкой на сущность User:

references diagram

Если ваше приложение запущено, остановите его с помощью кнопки Stop (suspend) на главной панели инструментов.

В окне инструментов Jmix нажмите New (add) → JPA Entity, как вы делали в предыдущей главе. В диалоговом окне New JPA Entity введите Department в поле Class и установите флажок TraitsVersioned:

create entity 1

Нажмите на кнопку OK.

Студия создаст класс сущности и откроет дизайнер сущностей:

create entity 2

Создание уникального атрибута

Давайте добавим атрибут name к сущности.

Нажмите Add (add) на панели Attributes. В диалоговом окне New Attribute введите name в поле Name и установите флажок Mandatory:

create entity 3

Теперь давайте определим уникальное ограничение для столбца NAME в базе данных. Это гарантирует, что не будет отделов с одинаковым названием.

Перейдите на вкладку Indexes в нижнем части дизайнера и нажмите New Index (add) на панели инструментов Database Indexes. Studio добавит строку в список индексов:

create entity 4

Выберите NAME в списке Available attributes и щелкните arrow right на панели инструментов, чтобы переместить атрибут в список Selected attributes.

Установите флажки Unique и Constraint в строке индекса и измените имя на 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 changelog.

Создание ссылочного атрибута

Теперь создайте ссылку на HR-менеджера отдела.

Нажмите Add (add) на панели Attributes. В диалоговом окне New Attribute введите hrManager в поле Name. Затем выберите:

  • Attribute type: ASSOCIATION

  • Type: User

  • Cardinality: Many 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-экранов

Давайте создадим CRUD-экраны для сущности Department.

Нажмите ScreensCreate screen на панели действий в верхней части дизайнера сущностей:

create screens 1

На первом шаге мастера создания экрана выберите шаблон Entity browser and editor screen:

screen wizard 1

Нажмите Next.

Примите предложенные значения на первых двух шагах мастера.

На шаге Entity browser fetch plan добавьте атрибут hrManager в фетч-план:

create screens 2

Теперь вы можете быть уверены, что ссылочная сущность User будет загружена вместе с сущностью Department и отображена на экране просмотра.

Если какой-либо атрибут не выбран в фетч-плане, Studio не создает для него визуальный компонент в генерируемых экранах.

Нажмите кнопку Next.

На следующем шаге Entity editor fetch plan этот атрибут будет выбран автоматически:

create screens 3

Нажмите кнопку Next.

Оставьте значения по умолчанию на шаге Localizable messages и нажмите Create.

Studio сгенерирует два экрана: Department.browse и Department.edit и откроет их исходный код. Закройте пока все вкладки редактора - позже в этой главе вы внесете некоторые изменения в созданные экраны.

Запуск приложения

Нажмите на кнопку Debug (start debugger) на главной панели инструментов.

Перед запуском приложения Studio сгенерирует Liquibase changelog:

run app 1

Как вы можете видеть, changelog содержит команды для создания таблицы DEPARTMENT, уникальное ограничение для столбца NAME и внешнего ключа, а также индекс для столбца HR_MANAGER_ID.

Нажмите на кнопку Save and run.

Студия выполнит changelog, затем соберет и запустит приложение.

Откройте http://localhost:8080 в вашем веб-браузере и войдите в приложение с учетными данными администратора (admin / admin).

Раскройте меню Application и нажмите на подпункт Departments. Вы увидите экран 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();
    }

Таким образом, когда поля firstName и lastName пусты, имя экземпляра пользователя - это username в квадратных скобках, как это видно в приложении на данный момент.

Дизайнер сущностей Studio автоматически генерирует аннотацию @InstanceName, если он встречает атрибут с соответствующим именем: name, description и так далее. Например, ваша сущность Department имеет @InstanceName в своем атрибуте name:

public class Department {
    // ...

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

Таким образом, название отдела будет отображаться в пользовательском интерфейсе, если вы используете отдел в качестве ссылки в другой сущности. Вы увидите это позже в самоучителе.

Дизайнер сущностей также поможет вам определить имя экземпляра вручную. Вы можете выбрать в качестве имени экземпляра какой-либо атрибут или сгенерировать метод, используя поле Instance name и кнопку рядом с ним:

instance name 1

Простая кастомизация UI

Автоматически сгенерированный CRUD UI для отделов выглядит приемлемо, но есть некоторые недочеты, которые стоит исправить.

Изменение заголовка атрибута

Возможно, вы заметили, что сгенерированный заголовок для атрибута hrManager не совсем корректен: он читается как Hr manager. Давайте изменим его на HR Manager.

Выберите атрибут hrManager в дизайнере сущностей и нажмите на кнопку глобуса (globe) рядом с именем атрибута:

change caption 1

Появится диалоговое окно Localized Message:

change caption 2

Измените текст и нажмите кнопку OK.

Вы можете просмотреть и отредактировать все сообщения вашего проекта, если дважды щелкните элемент User InterfaceMessage Bundle в окне инструментов Jmix. Сообщение, которое вы только что изменили, выделено ниже:

change caption 3

Переключитесь на приложение, запущенное в вашем веб-браузере. Закройте CRUD-экраны отдела и откройте их снова. Вы увидите новый заголовок для атрибута hrManager.

Благодаря технологии Studio Hot deploy вам не нужно перезапускать приложение при внесении изменений в пользовательский интерфейс.

Просто сохраните изменения в IDE (нажав комбинацию клавиш Ctrl/Cmd+S), выждите пару секунд, закройте и снова откройте измененный экран.

Обратите внимание, что обновление страницы веб-браузера не обновляет UI, так как состояние UI хранится на сервере. Для переоткрытия экрана закройте его вкладку внутри приложения и откройте ее снова из главного меню или из другого экрана.

Настройка действий EntityPicker

По умолчанию, когда вы нажимаете кнопку с многоточием в поле выбора HR-менеджера, экран выбора пользователя полностью закрывает редактор отдела. Давайте изменим поведение поля выбора, чтобы отображать экран пользователей в диалоговом окне.

Найдите department-edit.xml в окне инструментов Jmix и дважды щелкните по нему. Появится дизайнер экрана:

customize ui 1

В зависимости от разрешения вашего дисплея вы можете захотеть одновременно отображать только редактор XML или экран предварительного просмотра. Используйте кнопки в верхней части панели редактора для переключения режима:

customize ui 2

Переключитесь в режим Editor and Preview. Найдите поле hrManagerField на панели иерархии Jmix UI. Компонент будет выбран в предварительном просмотре, в редакторе XML и на панели инспектора Jmix UI в нижнем правом углу:

customize ui 3

Вы можете видеть, что элемент entityPicker содержит вложенный элемент actions с двумя действиями. Каждое действие соответствует кнопке поля выбора: действие entityLookup показывает экран для выбора связанной сущности, а действие entityClear очищает текущее значение поля выбора.

Действия можно настроить, указав специальные свойства.

Выберите действие entityLookup на панели иерархии Jmix UI, затем выберите значение DIALOG из выпадающего списка свойства openMode в панели инспектора Jmix UI:

customize ui 4

Ваши изменения будут отражены в XML.

Это работает и в противоположном направлении. Вы можете редактировать XML напрямую и просматривать результаты на панелях дизайнера и экране предварительного просмотра.

Переключитесь на запущенное приложение. Выберите отдел и нажмите Edit чтобы открыть экран редактора отдела. Далее нажмите кнопку с многоточием в поле выбора HR-менеджера.

Экран просмотра пользователей откроется в подвижном диалоговом окне:

customize ui 5

Изменение уникального сообщения о нарушении ограничений

Если вы попытаетесь создать другой отдел с тем же именем, вы увидите сообщение об ошибке нарушения уникального ограничения:

customize ui 8

Сообщение по умолчанию не очень дружественное к пользователю, но вы можете легко изменить его.

Дважды щелкните элемент User InterfaceMessage Bundle в окне инструментов Jmix и добавьте следующую строку:

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

Ключ сообщения должен начинаться с databaseUniqueConstraintViolation. и заканчиваться именем уникального ограничения базы данных. Вы можете заметить, что файл уже содержит аналогичное сообщение для уникального ограничения атрибута username сущности User.

Переключитесь на приложение и протестируйте свои изменения. Теперь в тексте ошибки отображено ваше сообщение:

customize ui 9

Резюме

В этом разделе вы реализовали вторую функцию: управление отделами.

Вы узнали, что:

  • Studio помогает создавать ссылочные атрибуты и генерирует Liquibase changelog с внешним ключом и индексом.

  • Чтобы показать ссылочный атрибут на экране просмотра или редактирования, он должен быть включен в фетч-план экрана.

  • Имя экземпляра используется для отображения ссылки в пользовательском интерфейсе.

  • Компонент выбора сущности (EntityPicker) используется по умолчанию для выбора связанной сущности в сгенерированном экране редактирования. Его действия можно настроить, например, показывать экран поиска в диалоговом окне.

  • Уникальность атрибутов сущностей поддерживается на уровне базы данных путем определения уникальных ограничений.

  • Уникальное сообщение о нарушении ограничений может быть легко кастомизировано.

  • Заголовки и сообщения, сгенерированные Studio, хранятся в пакете сообщений приложения.

  • Механизм Studio hot deploys изменяет экраны и сообщения в запущенном приложение, что избавляет от перезапуска приложения при разработке пользовательского интерфейса. Hot deploy не работает для классов сущностей.