Предисловие

Данное руководство содержит справочную информацию по платформе CUBA и охватывает наиболее важные темы разработки бизнес-приложений на платформе.

Для успешной работы с платформой требуется знание следующих технологий:

  • Java Standard Edition

  • Реляционные базы данных (SQL, DDL)

Для глубокого понимания принципов работы платформы полезным является знакомство со следующими технологиями и фреймворками:

Если у Вас имеются предложения по улучшению данного руководства, мы будем рады принять ваши pull request’ы и issues в исходниках документации на GitHub. Если вы увидели ошибку или несоответствие в документе - пожалуйста, форкните репозиторий и исправьте проблему. Заранее спасибо!

1. Установка и настройка

Системные требования
  • 64-битная операционная система - Windows, Linux или macOS.

  • Оперативная память - 8 ГБ для ведения разработки в CUBA Studio.

  • Свободное место на жестком диске - 10 ГБ.

Java SE Development Kit (JDK)
  • Установите JDK 8 и проверьте его работоспособность, выполнив в консоли команду

    java -version

    В ответ должно быть выведено сообщение с номером версии Java, например 1.8.0_202.

    CUBA 7.2 поддерживает Java 8, 9, 10 и 11. Если вам не нужно работать с проектами, основанными на предыдущих версиях CUBA (в том числе для их миграции на CUBA 7.2), то рекомендуется вместо Java 8 использовать Java 11.

    Обратите внимание, что OpenJ9 JVM не поддерживается.

  • Установите путь к корневому каталогу JDK в переменной окружения JAVA_HOME, например C:\Java\jdk8u202-b08.

    • Для Windows это можно сделать, открыв КомпьютерСвойства системыДополнительные параметры системыДополнительноПеременные среды, и задав значение переменной в списке Системные переменные.

    • Для macOS рекомендуется установить JDK в каталог /Library/Java/JavaVirtualMachines, например /Library/Java/JavaVirtualMachines/jdk8u202-b08, и задать JAVA_HOME в ~/.bash_profile следующей командой:

      export JAVA_HOME="$(/usr/libexec/java_home -v 1.8)"

  • Если для соединения с интернетом используется прокси-сервер, в JVM, исполняющие инструменты разработки и Gradle, необходимо передавать специальные системные свойства Java, описанные в документе http://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html (см. свойства для протоколов HTTP и HTTPS). Рекомендуется установить нужные свойства в переменной окружения JAVA_OPTS для всей операционной системы.

Cредства разработки

Следующие инструменты упрощают разработку на фреймворке CUBA:

  • CUBA Studio - интегрированная среда разработки (IDE), построенная на платформе IntelliJ и содержащая все необходимое для работы с проектами на CUBA. Ее можно установить либо как отдельное приложение для вашей операционной системы, либо в виде плагина для IntelliJ IDEA (Community or Ultimate). Подробнее см. CUBA Studio User Guide.

  • CUBA CLI - инструмент командной строки, предоставляющий базовую функциональность для создания проектов и их элементов: сущностей, экранов, сервисов и т.д. Этот инструмент позволяет использовать для разработки приложений на CUBA любую Java IDE. Подробнее см. страницу CUBA CLI на GitHub.

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

База данных

В простейшем случае в качестве сервера баз данных приложений используется встроенный HyperSQL (http://hsqldb.org), что вполне подходит для исследования возможностей платформы и прототипирования приложений. Для создания реальных приложений рекомендуется установить и использовать в проекте какую-либо из полноценных СУБД, поддерживаемых платформой, например PostgreSQL.

Веб-браузер

Веб-интерфейс приложений, создаваемых на основе платформы, поддерживает все популярные современные браузеры, в том числе Google Chrome, Mozilla Firefox, Safari, Opera 15+, Internet Explorer 11, Microsoft Edge.

2. Быстрый старт

На сайте платформы в разделе Быстрый старт рассматриваются основные шаги, необходимые для разработки приложения: создание проекта и базы данных, проектирование модели данных и создание пользовательского интерфейса. На вашей рабочей машине уже должно быть установлено и настроено необходимое программное обеспечение, см. Установка и настройка.

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

Следующим важным шагом в разработке приложения может быть реализация бизнес-логики. Смотрите руководство:

Для проектирования более сложной модели данных обратите внимание на следующие руководства:

Обзор использования чтения и записи данных в приложениях CUBA можно получить из руководства:

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

Большинство примеров кода в этом руководстве основаны на модели данных, используемой в приложении Sales.

Больше информации

ВИДЕО

Руководства и вебинары, детально объясняющие концепции и методики работы с платформой

РУКОВОДСТВА

Пошаговые руководства, объясняющие специфичную для платформы функциональность

ДЕМОПРИЛОЖЕНИЯ

Онлайн приложения, демонстрирующие функциональность платформы

3. Устройство платформы

Данная глава содержит подробное описание архитектуры, компонентов и механизмов платформы.

3.1. Архитектура

В данной главе рассмотрена архитектура CUBA-приложений в различных разрезах: по уровням, блокам, модулям и компонентам.

3.1.1. Уровни и блоки приложения

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

На каждом уровне возможно создание одного или нескольких блоков приложения. Блок представляет собой обособленную исполняемую программу, взаимодействующую с другими блоками приложения. Обычно блоки реализуются в виде веб-приложений выполняющихся на JVM.

AppTiers
Рисунок 1. Уровни и блоки приложения
Middleware

Средний слой, содержащий основную бизнес-логику приложения и выполняющий обращения к базе данных. Представляет собой отдельное веб-приложение под управлением стандартного контейнера сервлетов Java. См. Компоненты среднего слоя.

Web Client

Основной блок клиентского уровня. Содержит интерфейс, предназначенный, как правило, для внутренних (back-office) пользователей организации. Представляет собой отдельное веб-приложение под управлением стандартного контейнера сервлетов Java. Реализация пользовательского интерфейса основана на фреймворке Vaadin. См. Универсальный пользовательский интерфейс.

Web Portal

Дополнительный блок клиентского уровня. Может содержать интерфейс для внешних пользователей и средства интеграции с мобильными устройствами и сторонними приложениями. Представляет собой отдельное веб-приложение под управлением стандартного контейнера сервлетов Java. Реализация пользовательского интерфейса основана на фреймворке Spring MVC. См. Компоненты портала.

Frontend UI

Дополнительный клиентский блок, предоставляющий интерфейс для внешних пользователей. В отличие от Web Portal, является приложением, выполняющимся на стороне клиента (пример: JavaScript-приложение, выполняющееся в веб-браузере). Работает со средним слоем через REST API, запущенный в блоке Web Client или Web Portal. Может быть основан на React или других фронтенд библиотеках и фреймворках. См. Фронтенд интерфейс.

Обязательным блоком любого приложения является средний слой - Middleware. Для реализации пользовательского интерфейса, как правило, используется один или несколько клиентских блоков, например, Web Client и Web Portal.

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

3.1.2. Модули приложения

Модуль – наименьшая структурная единица CUBA-приложения. Представляет собой один модуль проекта приложения и соответствующий ему JAR файл с исполняемым кодом.

Стандартные модули:

  • global – включает в себя классы сущностей, интерфейсы сервисов и другие общие для всех уровней классы. Используется во всех блоках приложения.

  • core – реализация сервисов и всех остальных компонентов среднего слоя.

  • gui – общие компоненты Универсальный пользовательский интерфейс. Используется в модуле Web Client.

  • web – реализация универсального пользовательского интерфейса на Vaadin.

  • portal – опциональный модуль – реализация веб-портала на Spring MVC.

  • front – опциональный модуль – реализация Фронтенд интерфейс на JavaScript.

AppModules
Рисунок 2. Модули приложения

3.1.3. Компоненты приложения

Фреймворк позволяет разделять функциональность приложения на компоненты. Каждый компонент (называемый также add-on) может иметь собственную модель данных, бизнес-логику и пользовательский интерфейс. Приложение использует компоненты как библиотеки и включает их функциональность.

Концепция компонентов приложения позволяет нам сохранять фреймворк относительно небольшим, и при этом предоставлять опциональную бизнес-функциональность в компонентах, таких как Reporting, Full-Text Search, Charts, WebDAV и других. В то же время, разработчики приложений могут использовать этот же механизм для декомпозиции больших проектов в набор функциональных модулей, которые разрабатываются независимо и имеют различный цикл релизов. Естественно, компоненты приложений могут быть переиспользуемыми и обеспечивать проблемно-специфический уровень абстракции поверх фреймворка.

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

Ниже приведена диаграмма зависимостей между стандартными компонентами, часто используемыми в приложении. Сплошными линиями изображены обязательные зависимости, пунктирными − опциональные.

BaseProjects

Ниже приведена диаграмма возможной структуры зависимостей между стандартными и кастомными компонентами приложения.

AppComponents2

Любое CUBA-приложение может быть легко превращено в компонент и предоставлять функциональность другому приложению. Для того чтобы приложение можно было использовать в качестве компонента, оно должно содержать дескриптор app-component.xml и специальный элемент в манифесте JAR модуля global. CUBA Studio позволяет автоматически сгенерировать дескриптор и манифест для текущего проекта.

Практическое руководство по работе с собственным компонентом приложения приведено в разделе Пример создания и использования компонента.

3.1.4. Состав приложения

Вышеописанные архитектурные принципы напрямую отражаются в структуре собранного приложения. Рассмотрим ее на примере простого приложения, которое состоит из двух блоков – Middleware и Web Client; и включает в себя функциональность компонентов cuba и reports.

SampleAppArtifacts
Рисунок 3. Состав простого приложения

На рисунке изображено содержимое некоторых каталогов сервера Tomcat с развернутым в нем приложением.

Блок Middleware реализован веб-приложением app-core, блок Web Client – веб-приложением app. Исполняемый код веб-приложений содержится в каталогах WEB-INF/lib в наборе JAR-файлов. Каждый JAR представляет собой результат сборки (артефакт) одного из модулей приложения или его компонента.

Например, состав JAR-файлов веб-приложения среднего слоя app-core определяется тем, что блок Middleware состоит из модулей global и core, и приложение использует компоненты cuba и reports.

3.2. Общие компоненты

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

3.2.1. Модель данных

Предметная область моделируется в приложении с помощью взаимосвязанных классов Java, называемых классами сущностей или просто сущностями.

Сущности подразделяются на две категории:

  • Персистентные – экземпляры таких сущностей хранятся в таблицах базы данных с помощью ORM.

  • Неперсистентные – экземпляры существуют только в оперативной памяти, или сохраняются где-то с помощью иных механизмов.

Руководство Data Modelling: Many-to-Many Association демонстрирует различные варианты использования ассоциаций many-to-many. В руководстве Data Modelling: Composition приведены примеры композиции между сущностями.

Сущности характеризуются своими атрибутами. Атрибут соответствует полю класса и паре методов доступа (get / set) к полю. Чтобы атрибут был неизменяемым (read only), достаточно не создавать метод set.

Персистентные сущности могут включать в себя атрибуты, не хранящиеся в БД. В случае неперсистентного атрибута можно не создавать поле класса, ограничившись методами доступа.

Класс сущности должен удовлетворять следующим требованиям:

  • Наследоваться от одного из базовых классов, предоставляемых платформой (см. ниже).

  • Иметь набор полей и методов доступа, соответствующих атрибутам сущностей.

  • Класс и его поля (или методы доступа при отсутствии для атрибута соответствующего поля) должны быть определенным образом аннотированы для предоставления нужной информации фреймворкам JPA (в случае персистентной сущности) и метаданных.

Поддерживаются следующие типы атрибутов сущностей:

  • java.lang.String

  • java.lang.Boolean

  • java.lang.Integer

  • java.lang.Long

  • java.lang.Double

  • java.math.BigDecimal

  • java.util.Date

  • java.time.LocalDate

  • java.time.LocalTime

  • java.time.LocalDateTime

  • java.time.OffsetTime

  • java.time.OffsetDateTime

  • java.sql.Date

  • java.sql.Time

  • java.util.UUID

  • byte[]

  • enum

  • Cущность

Базовые классы сущностей (см. ниже) переопределяют equals() и hashCode() таким образом, что экземпляры сущностей сравниваются по их идентификаторам. То есть экземпляры одного класса считаются равными, если равны их идентификаторы.

3.2.1.1. Базовые классы сущностей

Рассмотрим базовые классы и интерфейсы сущностей более подробно.

EntityClasses
  • Instance – декларирует базовые методы работы с объектами предметной области:

    • Получение ссылки на мета-класс объекта.

    • Генерация имени экземпляра.

    • Чтение/установка значений атрибутов по имени.

    • Добавление слушателей, получающих уведомления об изменениях атрибутов.

  • Entity – дополняет Instance понятием идентификатора сущности, причем Entity не определяет тип идентификатора, оставляя эту возможность наследникам.

  • AbstractInstance – реализует логику работы со слушателями изменения атрибутов.

    AbstractInstance хранит слушателей в коллекции WeakReference, т.е. при отсутствии внешних ссылок на добавленного слушателя, он будет немедленно уничтожен сборщиком мусора. Как правило, слушателями изменения атрибутов являются визуальные компоненты и компоненты данных, на которые всегда имеются ссылки из других объектов, поэтому проблема исчезновения слушателей не возникает. Однако если слушатель создается прикладным кодом и на него никто не ссылается естественным образом, необходимо кроме добавления в Instance сохранить его в некотором поле объекта.

  • BaseGenericIdEntity - базовый класс персистентных и неперсистентных сущностей. Реализует Entity, но не специфицирует тип идентификатора (то есть первичного ключа) сущности.

  • EmbeddableEntity - базовый класс персистентных встраиваемых сущностей.

Ниже рассмотрены базовые классы, от которых рекомендуется наследовать сущности. Неперсистентные сущности наследуются от тех же классов, что и персистентные. Фреймворк определяет, является ли сущность персистентной или нет по файлу, в котором зарегистрирован класс: persistence.xml или metadata.xml.

StandardEntity

Наследуйте от StandardEntity, если необходим стандартный набор свойств сущности: первичный ключ типа UUID, экземпляры должны содержать информацию о том, кто и когда создал и изменил их, необходима оптимистичная блокировка и мягкое удаление.

EntityClasses Standard
  • HasUuid – интерфейс сущностей имеющих глобальные уникальные идентификаторы

  • Versioned – интерфейс сущностей, поддерживающих оптимистичную блокировку

  • Creatable – интерфейс сущностей, для которых требуется сохранять информацию о том, кто и когда ее создал

  • Updatable – интерфейс сущностей, для которых требуется сохранять информацию о том, кто и когда изменял экземпляр в последний раз

  • SoftDelete – интерфейс сущностей, поддерживающих мягкое удаление

BaseUuidEntity

Наследуйте от BaseUuidEntity, если необходима сущность с идентификатором типа UUID, но не нужны все остальные свойства StandardEntity. Интерфейсы Creatable, Versioned и др. можно выборочно реализовать в конкретном классе сущности.

EntityClasses Uuid
BaseLongIdEntity

Наследуйте от BaseLongIdEntity или BaseIntegerIdEntity, если необходима сущность с идентификатором типа Long или Integer. Интерфейсы Creatable, Versioned и др. можно выборочно реализовать в конкретном классе сущности. Рекомендуется реализовать HasUuid, так как это позволяет платформе в некоторых случаях работать с сущностью более оптимально, а кроме того, сущность получает уникальный идентификатор в распределенном окружении.

EntityClasses Long
BaseStringIdEntity

Наследуйте от BaseStringIdEntity, если необходима сущность с идентификатором типа String. Интерфейсы Creatable, Versioned и др. можно выборочно реализовать в конкретном классе сущности. Рекомендуется реализовать HasUuid, так как это позволяет платформе в некоторых случаях работать с сущностью более оптимально, а кроме того, сущность получает уникальный идентификатор в распределенном окружении. В конкретном классе сущности, унаследованной от BaseStringIdEntity, необходимо задать атрибут-идентификатор типа String и добавить ему JPA-аннотацию @Id.

EntityClasses String
BaseIdentityIdEntity

Наследуйте от BaseIdentityIdEntity, если необходимо отобразить сущность на таблицу с первичным ключом типа IDENTITY. Интерфейсы Creatable, Versioned и др. можно выборочно реализовать в конкретном классе сущности. Рекомендуется реализовать HasUuid, так как это позволяет платформе в некоторых случаях работать с сущностью более оптимально, а кроме того, сущность получает уникальный идентификатор в распределенном окружении. Атрибут id сущности (т.е. методы getId()/setId()) будут иметь тип IdProxy, который предназначен для использования вместо реального идентификатора, пока он не сгенерирован базой данных на вставке записи.

EntityClasses Identity
BaseIntIdentityIdEntity

Наследуйте от BaseIntIdentityIdEntity, если необходимо отобразить сущность на таблицу с целочисленным первичным ключом типа IDENTITY (в отличие от Long в BaseIdentityIdEntity). В остальных отношениях BaseIntIdentityIdEntity повторяет BaseIdentityIdEntity.

EntityClasses IntIdentity
BaseGenericIdEntity

Наследуйте напрямую от BaseGenericIdEntity, если необходимо отобразить сущность на таблицу с композитным первичным ключом. В этом случае в классе сущности необходимо создать поле встраиваемого типа, представляющего композитный ключ, и аннотировать его JPA-аннотацией @EmbeddedId.

3.2.1.2. Аннотации сущностей

В данном разделе описаны все поддерживаемые платформой аннотации классов и атрибутов сущностей.

Аннотации пакета javax.persistence обеспечивают работу JPA, аннотации пакетов com.haulmont.* предназначены для управления метаданными и другими механизмами платформы.

Если для аннотации указано только простое имя класса, подразумевается что это класс платформы, расположенный в одном из пакетов com.haulmont.*

3.2.1.2.1. Аннотации класса
@Embeddable

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

Для задания имени сущности требуется применение аннотации @MetaClass.

@EnableRestore

Указывает, что экземпляры данной сущности доступны для восстановления после мягкого удаления в специальном экране core$Entity.restore, доступном через пункт Administration > Data Recovery главного меню.

@Entity

Объявляет класс сущностью модели данных.

Параметры:

  • name - имя сущности, обязательно должно начинаться с префикса, отделенного знаком _. Желательно использовать в качестве префикса короткое имя проекта для формирования отдельного пространства имен.

Пример:

@Entity(name = "sales_Customer")
@Extends

Указывает, что данная сущность является расширением и должна повсеместно использоваться вместо базовой. См. Расширение функциональности.

@DiscriminatorColumn

Используется для определения колонки БД, отвечающей за различение типов сущностей в случае стратегий наследования SINGLE_TABLE и JOINED.

Параметры:

  • name - имя колонки-дискриминатора

  • discriminatorType - тип данных колонки-дискриминатора

Пример:

@DiscriminatorColumn(name = "TYPE", discriminatorType = DiscriminatorType.INTEGER)
@DiscriminatorValue

Определяет значение колонки-дискриминатора для данной сущности. Эта аннотация должна быть помещена на конкретном классе сущности.

Пример:

@DiscriminatorValue("0")
@IdSequence

Явно задает имя последовательности базы данных, используемой для генерации идентификаторов сущности, если она является подклассом BaseLongIdEntity или BaseIntegerIdEntity. Если сущность не аннотирована, то фреймворк создает последовательность с автоматически сгенерированным именем.

Параметры:

  • name – имя последовательности.

  • cached - необязательный параметр, определяющий что последовательность должена инкрементироваться через cuba.numberIdCacheSize для кэширования промежуточных значений в памяти. По умолчанию false.

По умолчанию последовательности создаются в основном хранилище. Если же свойство приложения cuba.useEntityDataStoreForIdSequence установлено в true, последовательности будут создаваться в хранилище, к которому принадлежит данная сущность.

@Inheritance

Определяет стратегию наследования для иерархии классов сущностей. Данная аннотация должна быть помещена на корневом классе иерархии.

Параметры:

  • strategy - стратегия, по умолчанию SINGLE_TABLE

@Listeners

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

Значением аннотации должна быть строка или массив строк с именами бинов слушателей - см. Entity Listeners.

Примеры:

@Listeners("sample_UserEntityListener")
@Listeners({"sample_FooListener","sample_BarListener"})
@MappedSuperclass

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

Руководство Data Modelling: Entity Inheritance демонстрирует механизм наследования сущностей.

@MetaClass

Используется для объявления неперсистентной или встраиваемой сущности (т.е. когда аннотация @javax.persistence.Entity не применима)

Параметры:

  • name - имя сущности, обязательно должно начинаться с префикса, отделенного знаком _. Желательно использовать в качестве префикса короткое имя проекта для формирования отдельного пространства имен.

Пример:

@MetaClass(name = "sales_Customer")
@NamePattern

Определяет способ получения имени экземпляра, т.е. строки, которая представляет экземпляр сущности. Имя экземпляра можно описать как метод toString() прикладного уровня. Он повсеместно используется в UI при отображении экземпляров сущностей в отдельных полях подобных TextField или LookupField. Кроме того, имя экземпляра можно получить программно методом MetadataTools.getInstanceName().

Значением аннотации должна быть строка вида {0}|{1}, где:

  • {0} - строка форматирования, которая может быть одной из следующих двух типов:

    • Строка с символами %s для подстановки форматированных значений атрибутов. Значения форматируются в строки в соответствии с datatypes атрибутов.

    • Имя метода данного объекта с префиксом #. Метод должен возвращать String и не иметь параметров.

  • {1} - разделенный запятыми список имен атрибутов сущности, соответствующий формату {0}. В случае использования в {0} метода список полей все равно необходим, так как по нему формируется представление _minimal.

Примеры:

@NamePattern("%s|name")
@NamePattern("%s - %s|name,date")
@NamePattern("#getCaption|amount,customer")
...
public String getCaption(){
    String prefix = "";
    if (amount > 5000) {
        prefix = "Grade 1 ";
    } else {
        prefix = "Grade 2 ";
    }
    return prefix + customer.name;
}
@PostConstruct

Данная аннотация может быть указана для метода класса. Такой метод будет вызван сразу после создания экземпляра сущности методом Metadata.create() или аналогичными методами DataManager.create() и DataContext.create().

В руководстве Initial Entity Values приводится пример определения начального значения непосредственно в классе сущности с помощью аннотации @PostConstruct.

Аннотированный метод может принимать в качестве параметров Spring-бины, доступные в модуле global. Например:

@PostConstruct
public void postConstruct(Metadata metadata, SomeBean someBean) {
    // ...
}
@PrimaryKeyJoinColumn

Используется в случае стратегии наследования JOINED для указания колонки внешнего ключа данной сущности, ссылающегося на первичный ключ сущности-предка.

Параметры:

  • name - имя колонки внешнего ключа данной сущности

  • referencedColumnName - имя колонки первичного ключа сущности предка

Пример:

@PrimaryKeyJoinColumn(name = "CARD_ID", referencedColumnName = "ID")
@PublishEntityChangedEvents

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

@SystemLevel

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

@Table

Определяет таблицу базы данных для данной сущности.

Параметры:

  • name - имя таблицы

Пример:

@Table(name = "SALES_CUSTOMER")
@TrackEditScreenHistory

Указывает, что для данной сущности будет запоминаться история открытия экранов редактирования ({имя_сущности}.edit) с возможностью отображения в специальном экране sec$ScreenHistory.browse, который можно подключить к главному меню с помощью следующего элемента web-menu.xml:

<item id="sec$ScreenHistory.browse" insertAfter="settings"/>
3.2.1.2.2. Аннотации атрибутов

Аннотации атрибутов устанавливаются на соответствующие поля класса, за одним исключением: если требуется объявить неизменяемый (read only) неперсистентный атрибут foo, то достаточно создать метод доступа getFoo() и поместить на этот метод аннотацию @MetaProperty.

@CaseConversion

Применяет автоматическую конвертацию регистра к текстовым полям ввода, связанным с аннотированным атрибутом.

Параметры:

  • type - тип конвертации: UPPER (по умолчанию), LOWER.

Пример:

@CaseConversion(type = ConversionType.UPPER)
@Column(name = "COUNTRY_CODE")
protected String countryCode;
@Column

Определяет колонку БД, в которой будут храниться значения данного атрибута.

Параметры:

  • name - имя колонки

  • length - (необязательный параметр, по умолчанию 255) - длина колонки. Используется также при формировании метаданных и, в конечном счете, может ограничивать максимальную длину вводимого текста в визуальных компонентах, работающих с данным атрибутом. Для отмены ограничения по длине атрибуту необходимо добавить аннотацию @Lob.

  • nullable - (необязательный параметр, по умолчанию true) - может ли атрибут содержать null. При указании nullable = false JPA контролирует наличие значения поля при сохранении, кроме того, визуальные компоненты, работающие с данным атрибутом, могут сигнализировать пользователю о необходимости ввода значения.

@Composition

Указывает на то, что связь является композицией - более тесным вариантом ассоциации. Это означает, что связанная сущность имеет смысл только как часть владеющей сущности, т.е. создается и удаляется вместе с ней.

В руководстве Data Modelling: Composition приведены примеры композиции между сущностями.

Например, список пунктов в заказе (класс Order содержит коллекцию экземпляров Item):

@OneToMany(mappedBy = "order")
@Composition
protected List<Item> items;

Другой пример - one-to-one отношение:

@Composition
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "DETAILS_ID")
protected CustomerDetails details;

Указание для связи аннотации @Composition позволяет организовать в экранах редактирования специальный режим коммита источников данных, при котором изменения экземпляров детализирующей сущности сохраняются в базе данных только при коммите основной сущности. Подробнее см. Редактирование композитных сущностей.

@CurrencyValue

Указывает, что данный атрибут предназначен для хранения денежных единиц. Если указана эта аннотация, при генерации экрана редактирования сущности в Studio внутри Form будет автоматически использован компонент CurrencyField.

Параметры:

  • currency – текст, который будет отображаться в ярлыке валюты: например, USD, GBP, EUR, $.

  • labelPosition - определяет положение ярлыка внутри текстового поля: RIGHT (по умолчанию), LEFT.

Пример:

@CurrencyValue(currency = "$", labelPosition = CurrencyLabelPosition.LEFT)
@Column(name = "PRICE")
protected BigDecimal price;
@Embedded

Определяет атрибут типа встраиваемой сущности, в свою очередь аннотированной @Embeddable.

Пример:

@Embedded
protected Address address;
@EmbeddedParameters

По умолчанию ORM не создает экземпляр встроенной сущности если все ее атрибуты равны null в базе данных. Аннотацию @EmbeddedParameters можно использовать для указания того, что экземпляр всегда должен создаваться, например:

@Embedded
@EmbeddedParameters(nullAllowed = false)
protected Address address;
@Id

Указывает, что данный атрибут является первичным ключом сущности. Обычно эта аннотация присутствует на поле базового класса, такого как BaseUuidEntity. Использовать эту аннотацию в конкретном классе сущности необходимо только при наследовании от базового класса BaseStringIdEntity (то есть при создании сущности со строковым первичным ключом).

@IgnoreUserTimeZone

Для атрибутов типа timestamp с аннотацией @javax.persistence.Temporal.TIMESTAMP заставляет платформу игнорировать часовой пояс пользователя, если он задан для текущей сессии.

@JoinColumn

Используется для указания колонки БД, определяющей ассоциацию между сущностями. Наличие этой аннотации указывает, что данная сторона отношения является владеющей (owning).

Параметры:

  • name - имя колонки

Пример:

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
protected Customer customer;
@JoinTable

Используется для указания таблицы связи на ведущей стороне @ManyToMany ассоциации.

Параметры:

  • name - имя таблицы связи

  • joinColumns - элемент @JoinColumn, определяющий колонку таблицы связей, соответствующую первичному ключу ведущей стороны ассоциации (т.е. содержащей аннотацию @JoinTable)

  • inverseJoinColumns - элемент @JoinColumn, определяющий колонку таблицы связей, соответствующую первичному ключу ведомой стороны ассоциации

Пример атрибута customers класса Group, являющегося ведущей стороной ассоциации:

@ManyToMany
@JoinTable(name = "SALES_CUSTOMER_GROUP_LINK",
  joinColumns = @JoinColumn(name = "GROUP_ID"),
  inverseJoinColumns = @JoinColumn(name = "CUSTOMER_ID"))
protected Set<Customer> customers;

Пример атрибута groups класса Customer, являющегося ведомой стороной этой же ассоциации:

@ManyToMany(mappedBy = "customers")
protected Set<Group> groups;
@Lob

Указывает, что данный атрибут не имеет ограничений длины. Применяется совместно с аннотацией @Column. Если @Lob указан, то длина, заданная в @Column явно или по умолчанию, игнорируется.

Пример:

@Column(name = "DESCRIPTION")
@Lob
private String description;
@Lookup

Определяет тип просмотра ссылочных атрибутов.

Параметры:

  • type - по умолчанию имеет значение SCREEN, при котором ссылки открываются через lookup-экран. Значение DROPDOWN позволяет открывать ссылки в виде выпадающего списка. Если за способ отображения выбран DROPDOWN, Studio создаст options collection container для выпадающего списка при генерации экрана редактирования. Таким образом, параметр Lookup type необходимо задать ДО генерации экрана редактирования сущности. Кроме того, компонент Filter позволит пользователям выбирать параметры фильтрации также из выпадающего списка вместо lookup-экрана.

  • actions - определяет действия, которые будут использованы в компоненте PickerField в составе FieldGroup по умолчанию. Возможные значения: lookup, clear, open.

@Lookup(type = LookupType.DROPDOWN, actions = {"open"})
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
protected Customer customer;
@ManyToMany

Определяет атрибут-коллекцию ссылок на сущность с типом ассоциации много-ко-многим.

Руководство Data Modelling: Many-to-Many Association демонстрирует различные варианты использования ассоциаций many-to-many.

Ассоциация много-ко-многим может иметь ведущую сторону и обратную - ведомую. На ведущей стороне указывается дополнительная аннотация @JoinTable, на ведомой стороне - параметр mappedBy.

Параметры:

  • mappedBy - поле связанной сущности, определяющее ассоциацию с ведущей стороны. Необходимо указывать только на ведомой стороне.

  • targetEntity - тип связанной сущности. Необязательный параметр, если коллекция объявлена с использованием Java generics.

  • fetch - (необязательный параметр, по умолчанию LAZY) - определяет, будет ли JPA жадно загружать коллекцию связанных сущностей. Необходимо всегда оставлять значение по умолчанию LAZY, так как в CUBA-приложении политика загрузки связей определяется динамически на основе механизма представлений.

Использование параметра cascade аннотации не рекомендуется. Сущности, сохраняемые неявно при использовании такого объявления, будут пропущены некоторыми системными механизмами. В частности, бин EntityStates некорректно определяет для них состояние managed, а entity listeners не вызываются вообще.

@ManyToOne

Определяет атрибут-ссылку на сущность с типом ассоциации много-к-одному.

Параметры:

  • fetch - (по умолчанию EAGER) параметр, определяющий, будет ли JPA жадно загружать ассоциированную сущность. Данный параметр всегда должен быть установлен в значение LAZY, так как в CUBA-приложении политика загрузки связей определяется динамически на основе механизма представлений.

  • optional - (необязательный параметр, по умолчанию true) - может ли атрибут содержать null. При указании optional = false JPA контролирует наличие ссылки при сохранении, кроме того, визуальные компоненты, работающие с данным атрибутом, могут сигнализировать пользователю о необходимости ввода значения.

Например, несколько экземпляров Order (заказов) ссылаются на один экземпляр Customer (покупателя), в этом случае класс Order должен содержать следующее объявление:

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
protected Customer customer;

Использование параметра cascade аннотации не рекомендуется. Сущности, сохраняемые неявно при использовании такого объявления, будут пропущены некоторыми системными механизмами. В частности, бин EntityStates некорректно определяет для них состояние managed, а entity listeners не вызываются вообще.

@MetaProperty

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

Данная аннотация не обязательна для полей, снабженных следующими аннотациями пакета javax.persistence: @Column, @OneToOne, @OneToMany, @ManyToOne, @ManyToMany, @Embedded. Такие поля отражаются в метаданных автоматически. Поэтому @MetaProperty в основном применяется для определения неперсистентных атрибутов сущностей.

Параметры (опционально):

  • mandatory - может ли атрибут содержать null. При указании mandatory = true визуальные компоненты, работающие с данным атрибутом, могут сигнализировать пользователю о необходимости ввода значения.

  • datatype - явно задает datatype, чтобы переопределить datatype задаваемый Java-типом атрибута.

  • related - задает массив связанных персистентных атрибутов, которые должны быть загружены из БД, если данный атрибут включен в представление. Кроме того, если аннотация указана на методе-геттере, то есть атрибут сущности предназначен только для чтения, то изменения связанных атрибутов генерируют PropertyChangeEvent для данного неизменяемого атрибута. Эта особенность позволяет обновлять UI-компоненты, отображающие неизменяемые атрибуты, зависящие от других изменяемых атрибутов.

Пример использования для поля:

@Transient
@MetaProperty
protected String token;

Пример использования для метода:

@MetaProperty(related = "firstName,lastName")
public String getFullName() {
    return firstName + " " + lastName;
}
@NumberFormat

Задает формат атрибута типа Number (это может быть BigDecimal, Integer, Long или Double). Значения такого атрибута будут форматироваться в пользовательском интерфейсе в соответствии с указанными параметрами аннотации:

  • pattern - паттерн форматирования, задается по правилам, описанным в DecimalFormat.

  • decimalSeparator - символ, используемый в качестве разделителя целой и дробной части (опционально).

  • groupingSeparator - символ, используемый в качестве разделителя групп разрядов (optional).

Если decimalSeparator и/или groupingSeparator не указаны, фреймворк использует соответствующие значения из format strings для локали текущего пользователя. При форматировании без учета локали в этом случае используются символы из системной локали сервера.

Примеры:

@Column(name = "PRECISE_NUMBER", precision = 19, scale = 4)
@NumberFormat(pattern = "0.0000")
protected BigDecimal preciseNumber;

@Column(name = "WEIRD_NUMBER", precision = 19, scale = 4)
@NumberFormat(pattern = "#,##0.0000", decimalSeparator = "_", groupingSeparator = "`")
protected BigDecimal weirdNumber;

@Column(name = "SIMPLE_NUMBER")
@NumberFormat(pattern = "#")
protected Integer simpleNumber;

@Column(name = "PERCENT_NUMBER", precision = 19, scale = 4)
@NumberFormat(pattern = "#%")
protected BigDecimal percentNumber;
@OnDelete

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

Пример:

@OneToMany(mappedBy = "group")
@OnDelete(DeletePolicy.CASCADE)
private Set<Constraint> constraints;
@OnDeleteInverse

Определяет политику обработки связи в случае мягкого удаления сущности с обратной стороны ассоциации. См. Мягкое удаление.

Пример:

@ManyToOne
@JoinColumn(name = "DRIVER_ID")
@OnDeleteInverse(DeletePolicy.DENY)
private Driver driver;
@OneToMany

Определяет атрибут-коллекцию ссылок на сущность с типом ассоциации один-ко-многим.

Параметры:

  • mappedBy - поле связанной сущности, определяющее ассоциацию

  • targetEntity - тип связанной сущности. Необязательный параметр, если коллекция объявлена с использованием Java generics.

  • fetch - (необязательный параметр, по умолчанию LAZY) - определяет, будет ли JPA жадно загружать коллекцию связанных сущностей. Необходимо всегда оставлять значение по умолчанию LAZY, так как в CUBA-приложении политика загрузки связей определяется динамически на основе механизма представлений.

  • cascade - (необязательный параметр, по умолчанию {}) - каскадирование операций определяет, какие операции над сущностью должны быть применены к ассоциированным сущностям. Каскадирование на данном уровне не рекомендуется использовать.

Например, несколько экземпляров Item (пунктов заказа) ссылаются на один экземпляр Order (заказ) с помощью @ManyToOne поля Item.order, в этом случае класс Order может содержать коллекцию экземпляров Item:

@OneToMany(mappedBy = "order")
protected Set<Item> items;

Использование параметра cascade аннотации не рекомендуется. Сущности, сохраняемые неявно при использовании такого объявления, будут пропущены некоторыми системными механизмами. В частности, бин EntityStates некорректно определяет для них состояние managed, а entity listeners не вызываются вообще. Параметр orphanRemoval не принимает во внимание механизм мягкого удаления.

@OneToOne

Определяет атрибут-ссылку на сущность с типом ассоциации один-к-одному.

Параметры:

  • fetch - (по умолчанию EAGER) параметр, определяющий, будет ли JPA жадно загружать ассоциированную сущность. Данный параметр всегда должен быть установлен в значение LAZY, так как в CUBA-приложении политика загрузки связей определяется динамически на основе механизма представлений.

  • mappedBy - поле связанной сущности, определяющее ассоциацию. Требуется устанавливать только на ведомой стороне ассоциации.

  • optional - (необязательный параметр, по умолчанию true) - может ли атрибут содержать null. При указании optional = false JPA контролирует наличие ссылки при сохранении, кроме того, визуальные компоненты, работающих с данным атрибутом, могут сигнализировать пользователю о необходимости ввода значения.

Пример ведущей стороны ассоциации, класс Driver:

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CALLSIGN_ID")
protected DriverCallsign callsign;

Пример ведомой стороны ассоциации, класс DriverCallsign:

@OneToOne(fetch = FetchType.LAZY, mappedBy = "callsign")
protected Driver driver;
@OrderBy

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

Параметры:

  • value - строка, определяющая порядок, в формате:

    orderby_list::= orderby_item [,orderby_item]*
    orderby_item::= property_or_field_name [ASC | DESC]

Пример:

@OneToMany(mappedBy = "user")
@OrderBy("createTs")
protected List<UserRole> userRoles;
@Temporal

Для атрибута типа java.util.Date уточняет тип хранимого значения: дата, время или дата+время.

Параметры:

  • value - тип хранимого значения: DATE, TIME, TIMESTAMP

Пример:

@Column(name = "START_DATE")
@Temporal(TemporalType.DATE)
protected Date startDate;
@Transient

Указывает, что данное поле не хранится в БД, т.е. является неперсистентным.

Поля поддерживаемых JPA типов (см. http://docs.oracle.com/javaee/7/api/javax/persistence/Basic.html) по умолчанию являются персистентными, поэтому аннотация @Transient обязательна для объявления неперсистентного атрибута такого типа.

Для включения @Transient атрибута в метаданные, необходимо также указать аннотацию @MetaProperty.

@Version

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

Применение такого поля необходимо при реализации классом сущности интерфейса Versioned (базовый класс StandardEntity уже содержит такое поле).

Пример:

@Version
@Column(name = "VERSION")
private Integer version;
3.2.1.3. Атрибуты типа enum

В стандартном варианте использования JPA для атрибутов типа enum в базе данных хранится целое число, получаемое методом ordinal() этого перечисления. Такой подход может привести к следующим проблемам при эксплуатации и развитии системы:

  • при появлении в БД значения, не равного ни одному ordinal значению перечисления, экземпляр сущности нельзя загрузить вообще;

  • невозможно ввести новое значение между имеющимися, что актуально при использовании сортировки по значению перечисления (order by).

Чтобы решить эти проблемы, в подходе CUBA предлагается отвязать значение, хранимое в БД, от ordinal перечисления. Для этого необходимо поле класса сущности объявлять с типом, хранимым в БД (Integer или String), а методы доступа (getter / setter) создавать для типа самого перечисления.

Например:

@Entity(name = "sales_Customer")
@Table(name = "SALES_CUSTOMER")
public class Customer extends StandardEntity {

    @Column(name = "GRADE")
    protected Integer grade;

    public CustomerGrade getGrade() {
        return grade == null ? null : CustomerGrade.fromId(grade);
    }

    public void setGrade(CustomerGrade grade) {
        this.grade = grade == null ? null : grade.getId();
    }
    ...
}

При этом сам класс перечисления может выглядеть следующим образом:

public enum CustomerGrade implements EnumClass<Integer> {

    PREMIUM(10),
    HIGH(20),
    MEDIUM(30);

    private Integer id;

    CustomerGrade(Integer id) {
        this.id = id;
    }

    @Override
    public Integer getId() {
        return id;
    }

    public static CustomerGrade fromId(Integer id) {
        for (CustomerGrade grade : CustomerGrade.values()) {
            if (grade.getId().equals(id))
                return grade;
        }
        return null;
    }
}

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

Как видно из примеров, для атрибута grade в БД хранится значение типа Integer, задаваемое полем id перечисления CustomerGrade, а конкретно 10, 20 или 30. В то же время прикладной код и метаданные работают с самим типом CustomerGrade через методы доступа, которые и осуществляют конвертацию.

При наличии в поле БД значения, не соответствующего ни одному значению перечисления, метод getGrade() просто вернет null. Для ввода нового значения, например, HIGHER, между HIGH и PREMIUM, достаточно добавить это значение в перечисление с идентификатором 15, при этом сортировка по полю Customer.grade останется верной.

Тип поля Integer удобно использовать в случаях, когда необходим упорядоченный список констант, подлежащий сортировке, например, в запросах JPQL и SQL (>, <, >=, , order by), кроме того, он имеет незначительное преимущество перед String в плане производительности формата хранения и занимаемого места. С другой стороны, значения типа Integer сами по себе неочевидны и могут затруднять отладку и интерпретацию результатов запросов, они неудобны в работе с голыми данными и сериализованными форматами. Если отношение упорядочения между константами не требуется, удобнее использовать тип String.

Перечисления могут быть созданы в CUBA Studio в секции Data Model > New > Enumeration. Чтобы использовать перечисление в качестве атрибута сущности, в редакторе атрибута нужно выбрать ENUM в поле Attribute type и класс перечисления в поле Type. Значениям перечисления могут быть сопоставлены локализованные названия для отображения в пользовательском интерфейсе приложения.

3.2.1.4. Мягкое удаление

Платформа CUBA поддерживает режим "мягкого удаления" данных - когда вместо удаления записей из базы данных они только помечаются определенным образом и становятся недоступными для обычного использования. В дальнейшем такие записи можно либо совсем удалить из БД с помощью отдельной регламентной процедуры, либо восстановить.

Механизм мягкого удаления является "прозрачным" для прикладного программиста - достаточно убедиться, что класс сущности реализует интерфейс SoftDelete, и платформа сама нужным образом будет модифицировать запросы и операции с данными.

Режим мягкого удаления имеет следующие преимущества:

  • значительно снижается риск потери данных вследствие неверных действий пользователей

  • позволяет быстро сделать некоторые записи недоступными, даже если на них имеются ссылки.

    Возьмем для примера модель данных Заказы - Покупатели. Допустим, на некоторого покупателя оформлено несколько заказов, однако нам нужно сделать его недоступным для дальнейшей работы пользователей. Традиционным "жестким" удалением сделать это невозможно, так как для удаления покупателя нам нужно либо удалить все его заказы, либо обнулить в этих заказах ссылки на него (т.е. потерять информацию). При мягком удалении покупателя он становится недоступным для поиска и изменения, однако при просмотре заказов пользователь видит на экране имя покупателя, так как при загрузке связей признак удаления намеренно игнорируется.

    Описанное поведение является стандартным, но может быть модифицировано с помощью политики обработки связей при удалении.

Восстановить удалённые сущности можно через экран Restore Deleted Entities, по умолчанию доступный в стандартном меню Administration приложения. Эта функциональность предназначена для использования администраторами системы, имеющими разрешения на все сущности. Её следует использовать с осторожностью, также рекомендуется ограничить доступ к этому экрану для простых пользователей системы.

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

3.2.1.4.1. Использование

Для того чтобы экземпляры сущности удалялись мягко, класс сущности должен реализовывать интерфейс SoftDelete, а соответствующая таблица БД должна содержать колонки:

  • DELETE_TS - когда удалена запись

  • DELETED_BY - логин пользователя, который удалил запись

Поведение системы по умолчанию - сущности, реализующие SoftDelete, удаляются мягко, удаленные сущности не возвращаются запросами и поиском по идентификатору. При необходимости такое поведение можно динамически отключить следующими способами:

  • для текущего экземпляра EntityManager - вызовом setSoftDeletion(false)

  • при запросе данных через DataManager - вызовом у передаваемого объекта LoadContext метода setSoftDeletion(false)

  • на уровне загрузчиков данных - используя метод DataLoader.setSoftDeletion(false) или атрибут softDeletion="false" элемента loader в XML-дескрипторе экрана.

В режиме мягкого удаления платформа автоматически отфильтровывает удаленные экземпляры при загрузке по идентификатору и по JPQL-запросу, а также удаленные элементы связанных сущностей в атрибутах-коллекциях. Однако связанные сущности в единичных (*ToOne) атрибутах загружаются независимо от того, удален связанный экземпляр или нет.

3.2.1.4.2. Политика обработки связей

Для мягко удаляемых сущностей, платформа предоставляет средство обработки связей при удалении экземпляров, во многом аналогичное правилам ON DELETE внешних ключей в базе данных. Это средство работает на уровне Middleware и использует аннотации @OnDelete, @OnDeleteInverse атрибутов сущности.

Аннотация @OnDelete обрабатывается при удалении той сущности, в которой она встретилась, а не той, на которую указывает аннотированный атрибут (в этом отличие от каскадных удалений на уровне БД).

Аннотация @OnDeleteInverse обрабатывается при удалении той сущности, на которую указывает аннотированный атрибут, (т.е. аналогично каскадному удалению на уровне внешних ключей в БД). Эта аннотация полезна при отсутствии в удаляемом объекте атрибута, который нужно проверять при удалении. При этом, как правило, в проверяемом объекте существует ссылка на удаляемый, на этот атрибут и устанавливается аннотация @OnDeleteInverse.

Значением аннотации может быть:

  • DeletePolicy.DENY - запретить удаление сущности, если аннотированный атрибут не null или не пустая коллекция

  • DeletePolicy.CASCADE - каскадно удалить аннотированный атрибут

  • DeletePolicy.UNLINK - разорвать связь с аннотированным атрибутом. Разрыв связи имеет смысл указывать только на ведущей стороне ассоциации - той, которая в классе сущности аннотирована @JoinColumn.

Примеры:

  1. Запрет удаления при наличии ссылки: при попытке удаления экземпляра Customer, на который ссылается хотя бы один Order, будет выброшено исключение DeletePolicyException.

    Order.java

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CUSTOMER_ID")
    @OnDeleteInverse(DeletePolicy.DENY)
    protected Customer customer;

    Customer.java

    @OneToMany(mappedBy = "customer")
    protected List<Order> orders;

    Сообщения в окне исключения могут быть локализованы в главном пакете сообщений. Используйте для этого следующие ключи:

    • deletePolicy.caption - заголовок уведомления.

    • deletePolicy.references.message - тело сообщения.

    • deletePolicy.caption.sales_Customer - заголовок уведомления для конкретной сущности.

    • deletePolicy.references.message.sales_Customer - тело сообщения для конкретной сущности.

  2. Каскадное удаление элементов коллекции: при удалении экземпляра Role все экземпляры Permission также будут удалены.

    Role.java

    @OneToMany(mappedBy = "role")
    @OnDelete(DeletePolicy.CASCADE)
    protected Set<Permission> permissions;

    Permission.java

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ROLE_ID")
    protected Role role;
  3. Разрыв связи с элементами коллекции: удаление экземпляра Role приведет к установке в null ссылок со стороны всех входивших в коллекцию экземпляров Permission.

    Role.java

    @OneToMany(mappedBy = "role")
    protected Set<Permission> permissions;

    Permission.java

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ROLE_ID")
    @OnDeleteInverse(DeletePolicy.UNLINK)
    protected Role role;

Особенности реализации:

  1. Политика обработки связей обрабатывается на уровне Middleware при сохранении сущностей, реализующих интерфейс SoftDelete, в БД.

  2. Нужно быть осторожным при использовании @OnDeleteInverse с политиками CASCADE и UNLINK, так как при этом происходит извлечение из БД на сервер приложения всех экземпляров ссылающихся объектов, изменение и затем сохранение.

    Например, в случае ассоциации Customer - Job и большого количества работ для одного заказчика, если поставить на атрибут Job.customer политику @OnDeleteInverse(CASCADE), то при удалении экземпляра заказчика будет предпринята попытка извлечь и изменить все его работы. Это может привести к перегрузке сервера приложения и БД.

    С другой стороны, использование @OnDeleteInverse(DENY) безопасно, так как при этом производится только подсчет количества ссылающихся объектов, и если оно больше 0, выбрасывается исключение. Поэтому @OnDeleteInverse(DENY) для атрибута Job.customer вполне допустимо.

3.2.1.4.3. Ограничение уникальности на уровне БД

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

Реализуется данная логика путем, специфичным для используемого сервера базы данных:

  • Если сервер БД поддерживает частичные (partial) индексы (например, PostgreSQL), то ограничение уникальности можно создать следующим образом:

    create unique index IDX_SEC_USER_UNIQ_LOGIN on SEC_USER (LOGIN_LC) where DELETE_TS is null
  • Если сервер БД не поддерживает частичные индексы (например, Microsoft SQL Server 2005), то в уникальный индекс можно включить поле DELETE_TS:

create unique index IDX_SEC_USER_UNIQ_LOGIN on SEC_USER (LOGIN_LC, DELETE_TS)

3.2.2. Metadata Framework

Для эффективной работы с моделью данных в CUBA-приложениях используется фреймворк метаданных, который:

  • предоставляет удобный интерфейс для получения информации о сущностях, их атрибутах и отношениях между сущностями; а также для навигации по ссылкам

  • служит специализированной и более удобной в использовании альтернативой Java Reflection API

  • регламентирует допустимые типы данных и отношений между сущностями

  • позволяет создавать универсальные механизмы работы с данными

3.2.2.1. Интерфейсы метаданных

Рассмотрим основные интерфейсы метаданных.

MetadataFramework
Рисунок 4. Интерфейсы фреймворка метаданных
Session

Точка входа в фреймворк метаданных. Позволяет получать экземпляры MetaClass по имени и по соответствующему классу Java. Обратите внимание на различие методов getClass() и getClassNN() - первые могут возвращать null, вторые нет (NonNull).

Объект Session может быть получен через интерфейс инфраструктуры Metadata.

Пример:

@Inject
protected Metadata metadata;
...
Session session = metadata.getSession();
MetaClass metaClass1 = session.getClassNN("sec$User");
MetaClass metaClass2 = session.getClassNN(User.class);
assert metaClass1 == metaClass2;
MetaModel

Редко используемый интерфейс, служит для группировки мета-классов.

Группировка осуществляется по имени корневого Java пакета проекта, указываемого в файле metadata.xml.

MetaClass

Интерфейс метаданных класса сущности. MetaClass всегда ассоциирован с классом Java, которого он представляет.

Основные методы:

  • getName() – имя сущности, по соглашению первой частью имени до знака _ является код пространства имен, например, sales_Customer

  • getProperties() – список мета-свойств (MetaProperty)

  • getProperty(), getPropertyNN() - получение мета-свойства по имени. Первый метод в случае отсутствия атрибута с указанным именем возвращает null, второй выбрасывает исключение.

    Пример:

    MetaClass userClass = session.getClassNN(User.class);
    MetaProperty groupProperty = userClass.getPropertyNN("group");
  • getPropertyPath() - позволяет перемещаться по ссылкам. Данный метод принимает строковый параметр - путь из имен атрибутов, разделенных точкой. Возвращаемый объект MetaPropertyPath позволяет обратиться к искомому (последнему в пути) атрибуту вызовом getMetaProperty().

    Пример:

    MetaClass userClass = session.getClassNN(User.class);
    MetaProperty groupNameProp = userClass.getPropertyPath("group.name").getMetaProperty();
    assert groupNameProp.getDomain().getName().equals("sec$Group");
  • getJavaClass() – класс сущности, которому соответствует данный MetaClass

  • getAnnotations() – коллекция мета-аннотаций

MetaProperty

Интерфейс метаданных атрибута сущности.

Основные методы:

  • getName() – имя свойства, соответствует имени атрибута сущности

  • getDomain() – мета-класс, которому принадлежит данное свойство

  • getType() – тип свойства:

    • простой тип: DATATYPE

    • перечисление: ENUM

    • ссылочный тип двух видов:

      • ASSOCIATION − простая ссылка на другую сущность. Например, отношение заказа и покупателя − ассоциация.

      • COMPOSITION − ссылка на сущность, которая не имеет самостоятельного значения без владеющей сущности. COMPOSITION можно считать "более тесным" отношением, чем ASSOCIATION. Например, отношение заказа и пункта этого заказа − COMPOSITION, т.к. пункт не может существовать без заказа, которому он принадлежит.

        Вид ссылочного атрибута ASSOCIATION или COMPOSITION влияет на режим редактирования сущности: в первом случае сохранение связанной сущности в базу данных происходит независимо, а во втором − связанная сущность сохраняется в БД только вместе с владеющей сущностью.

  • getRange() – интерфейс Range, детально описывающий тип данного атрибута

  • isMandatory() – признак обязательности атрибута. Используется, например, визуальными компонентами для сигнализации пользователю о необходимости ввода значения.

  • isReadOnly() – признак неизменности атрибута

  • getInverse() – для ссылочного атрибута возвращает мета-свойство с обратной стороны ассоциации, если таковое имеется

  • getAnnotatedElement() – поле (java.lang.reflect.Field) или метод (java.lang.reflect.Method), соответствующие данному атрибуту сущности

  • getJavaType() – класс Java данного атрибута сущности. Это либо тип поля класса, либо тип возвращаемого значения метода.

  • getDeclaringClass() – класс Java, содержащий данный атрибут

Range

Интерфейс, детально описывающий тип атрибута сущности.

Основные методы:

  • isDatatype() – возвращает true для атрибута простого типа

  • asDatatype() - возвращает Datatype для атрибута простого типа

  • isEnum() – возвращает true для атрибута типа перечисления

  • asEnumeration() - возвращает Enumeration для атрибута типа перечисления

  • isClass() – возвращает true для ссылочного атрибута типа ASSOCIATION или COMPOSITION

  • asClass() - возвращает мета-класс ассоциированной сущности для ссылочного атрибута

  • isOrdered() – возвращает true если атрибут представляет собой упорядоченную коллекцию (например, List)

  • getCardinality() – вид отношения для ссылочного атрибута: ONE_TO_ONE, MANY_TO_ONE, ONE_TO_MANY, MANY_TO_MANY

3.2.2.2. Формирование метаданных

Основной источник формирования структуры метаданных - аннотированные классы сущностей.

Класс сущности отражается в метаданных в следующих случаях:

  • Класс персистентной сущности аннотирован @Entity, @Embeddable, @MappedSuperclass и расположен в пределах корневого пакета, указанного в metadata.xml.

  • Класс неперсистентной сущности аннотирован @MetaClass и расположен в пределах корневого пакета, указанного в metadata.xml.

Все сущности внутри одного корневого пакета помещаются в один экземпляр MetaModel, которому присваивается имя этого пакета. Между сущностями внутри одной MetaModel можно устанавливать произвольные связи, между разными - в порядке объявления файлов metadata.xml в свойстве cuba.metadataConfig.

Атрибут сущности отражается в метаданных, если:

  • поле класса аннотировано @Column, @OneToOne, @OneToMany, @ManyToOne, @ManyToMany, @Embedded

  • поле класса или метод доступа на чтение (getter) аннотирован @MetaProperty

Параметры мета-класса и мета-свойств формируются на основе параметров перечисленных аннотаций, а также типов полей и методов класса. Кроме того, если у атрибута отсутствует метод доступа на запись (setter), атрибут становится неизменяемым (read only).

3.2.2.3. Datatype

Интерфейс Datatype определяет методы конвертации значение в строку и из строки (formatting & parsing). Каждый атрибут сущности, не являющийся ссылкой, имеет некоторый Datatype, который и используется фреймворком для конвертации значений данного атрибута.

Экземпляры Datatype регистрируются в бине DatatypeRegistry, который выполняет загрузку и инициализацию классов реализации Datatype из файлов metadata.xml компонентов приложения и самого проекта.

Datatype атрибута сущности может быть получен из соответствующего meta-property методом getRange().asDatatype().

Кроме конвертации значений атрибутов сущностей, зарегистрированные экземпляры Datatype могут быть использованы для преобразования в строку и из строки произвольных значений поддерживаемых типов. Для этого необходимо получить экземпляр Datatype из DatatypeRegistry с помощью его методов get(Class) или getNN(Class), передавая тип Java, который необходимо конвертировать.

Datatype сопоставляется атрибуту сущности по следующим правилам:

  • Как правило, атрибуту сопоставляется экземпляр Datatype, зарегистрированный в DatatypeRegistry и предназначенный для конвертации типа атрибута.

    Например, в данном случае атрибут amount получит BigDecimalDatatype:

    @Column(name = "AMOUNT")
    private BigDecimal amount;

    потому что в com/haulmont/cuba/metadata.xml есть следующий элемент:

    <datatype id="decimal" class="com.haulmont.chile.core.datatypes.impl.BigDecimalDatatype"
              default="true"
              format="0.####" decimalSeparator="." groupingSeparator=""/>
  • Для поля или метода можно задать аннотацию @MetaProperty, указав в ней атрибут datatype.

    Например, атрибут issueYear получит тип YearDatatype:

    @MetaProperty(datatype = "year")
    @Column(name = "ISSUE_YEAR")
    private Integer issueYear;

    если файл metadata.xml проекта содержит следующий элемент:

    <datatype id="year" class="com.company.sample.YearDatatype"/>

    Как видно, атрибут datatype аннотации @MetaProperty указывает на идентификатор, который использован при регистрации класса имплементации Datatype в файле metadata.xml.

Основные методы интерфейса Datatype:

  • format() - преобразовывает переданное значение в строку

  • parse() - преобразовывает строку в значение нужного типа

  • getJavaClass() – возвращает тип Java, для конвертации которого создан данный Datatype. Этот метод имеет реализацию по умолчанию, которая считывает значение аннотации @JavaClass, если она присутствует на классе.

Datatype определяет два набора методов для форматирования/парсинга: с учетом локали и без учета локали. Преобразование с учетом локали используется повсеместно в пользовательском интерфейсе, преобразование без учета локали используется в системных механизмах, например, для сериализации в REST API.

Форматы для преобразований без учета локали задаются в коде имплементации или в файле metadata.xml.

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

3.2.2.3.1. Строки форматов Datatype

Форматы для преобразований с учетом локали задаются в главном пакете локализованных сообщений проекта или его компонентов, следуя логике стандартных классов Java SE, таких как DecimalFormat (см. https://docs.oracle.com/javase/tutorial/i18n/format/decimalFormat.html) и SimpleDateFormat (см. https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html).

Форматы задаются в строках со следующими ключами:

  • numberDecimalSeparator - задает символ разделителя целой и дробной части для числовых типов, например:

    # использование запятой в качестве разделителя
    numberDecimalSeparator=,
  • numberGroupingSeparator - задает символ разделителя групп разрядов для числовых типов

    # использование пробела в качестве разделителя
    numberGroupingSeparator = \u0020
  • integerFormat - формат для типов Integer и Long

    # запретить отображение разделителя тысяч для целых чисел
    integerFormat = #0
  • doubleFormat - формат для типа Double. Обратите внимание, что символы десятичного разделителя и разделителя групп разрядов задаются отдельно в своих собственных ключах (см. выше).

    # округление до трёх цифр после десятичного разделителя
    doubleFormat=#,##0.###
  • decimalFormat - формат для типа BigDecimal. Обратите внимание, что символы десятичного разделителя и разделителя групп разрядов задаются отдельно в своих собственных ключах (см. выше).

    # всегда отображать две цифры после десятичного разделителя, например, в денежных единицах
    decimalFormat = #,##0.00
  • dateTimeFormat - формат для типа java.util.Date

    # формат дата+время для России
    dateTimeFormat = dd.MM.yyyy HH:mm
  • dateFormat - формат для типа java.sql.Date

    # формат даты для США
    dateFormat = MM/dd/yyyy
  • timeFormat - формат для типа java.sql.Time

    # формат времени часы:минуты
    timeFormat=HH:mm
  • offsetDateTimeFormat – формат для типа java.time.OffsetDateTime

    # формат дата+время с отклонением от времени по Гринвичу
    offsetDateTimeFormat = dd/MM/yyyy HH:mm Z
  • offsetTimeFormat – формат для типа java.time.OffsetTime

    # формат времени часы:минуты с отклонением от времени по Гринвичу
    offsetTimeFormat=HH:mm Z
  • trueString - строка, соответствующая Boolean.TRUE

    # альтернативное отображение булевых значений
    trueString = да
  • falseString - строка, соответствующая Boolean.FALSE

    # альтернативное отображение булевых значений
    falseString = нет

Форматы для используемых в приложении языков могут быть заданы в Studio. Для этого откройте экран Project Properties, нажмите кнопку в поле Available locales, затем нажмите Show data format strings.

Строки форматов могут быть получены из бина FormatStringsRegistry.

3.2.2.3.2. Пример специализированного Datatype

В качестве примера рассмотрим следующую задачу: в приложении есть атрибуты сущностей, хранящие годы в виде целых чисел. Пользователи должны иметь возможность просматривать и редактировать годы, причем если пользователь вводит только две цифры, приложение должно преобразовать их в год между 2000 и 2100. В противном случае, все введенное число считается годом.

Создайте класс в модуле global:

package com.company.sample.entity;

import com.google.common.base.Strings;
import com.haulmont.chile.core.annotations.JavaClass;
import com.haulmont.chile.core.datatypes.Datatype;

import javax.annotation.Nullable;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.Locale;

@JavaClass(Integer.class)
public class YearDatatype implements Datatype<Integer> {

    private static final String PATTERN = "##00";

    @Override
    public String format(@Nullable Object value) {
        if (value == null)
            return "";

        DecimalFormat format = new DecimalFormat(PATTERN);
        return format.format(value);
    }

    @Override
    public String format(@Nullable Object value, Locale locale) {
        return format(value);
    }

    @Nullable
    @Override
    public Integer parse(@Nullable String value) throws ParseException {
        if (Strings.isNullOrEmpty(value))
            return null;

        DecimalFormat format = new DecimalFormat(PATTERN);
        int year = format.parse(value).intValue();
        if (year > 2100 || year < 0)
            throw new ParseException("Invalid year", 0);
        if (year < 100)
            year += 2000;
        return year;
    }

    @Nullable
    @Override
    public Integer parse(@Nullable String value, Locale locale) throws ParseException {
        return parse(value);
    }
}

Затем добавьте элемент datatypes в файл metadata.xml вашего проекта:

<metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd">
    <datatypes>
        <datatype id="year" class="com.company.sample.entity.YearDatatype"/>
    </datatypes>
    <!-- ... -->
</metadata>

В элементе datatype можно также указать атрибут sqlType, содержащий SQL-тип вашей базы данных, подходящий для хранения значений нового типа. Этот SQL-тип будет использоваться CUBA Studio при генерации скриптов базы данных. Studio может автоматически определить SQL-тип для следующих типов Java:

  • java.lang.Boolean

  • java.lang.Integer

  • java.lang.Long

  • java.math.BigDecimal

  • java.lang.Double

  • java.lang.String

  • java.util.Date

  • java.util.UUID

  • byte[]

В нашем случае класс предназначен для работы с типом Integer (что декларируется аннотацией @JavaClass со значением Integer.class), поэтому атрибут sqlType можно не указывать.

Наконец, укажите новый тип данных для требуемых атрибутов (программно или с помощью интерфейса Studio):

@MetaProperty(datatype = "year")
@Column(name = "ISSUE_YEAR")
private Integer issueYear;

После выполнения перечисленных действий атрибут issueYear везде в приложении будет отображаться в нужном формате.

3.2.2.3.3. Пример форматирования даты в UI

Рассмотрим отображение атрибута Order.date в таблице браузера заказов.

order-browse.xml

<table id="ordersTable">
    <columns>
        <column id="date"/>
        <!--...-->

Атрибут date в классе Order определен с типом "дата":

@Column(name = "DATE", nullable = false)
@Temporal(TemporalType.DATE)
private Date date;

Если текущий пользователь зарегистрирован c русской локалью, то из главного пакета локализованных сообщений извлекается строка:

dateFormat=dd.MM.yyyy

В результате дата "2012-08-06" конвертируется в строку "06.08.2012" для отображения в ячейке таблицы.

3.2.2.3.4. Примеры форматирования дат и чисел в коде приложения

Если вам необходимо отформатировать или получить из строки значения BigDecimal, Integer, Long, Double, Boolean или Date учитывая локаль текущего пользователя, используйте бин DatatypeFormatter. Например:

@Inject
private DatatypeFormatter formatter;

void sample() {
    String dateStr = formatter.formatDate(dateField.getValue());
    // ...
}

Ниже приведены примеры использования методов интерфейса Datatype напрямую.

  • Пример форматирования даты

    @Inject
    protected UserSessionSource userSessionSource;
    @Inject
    protected DatatypeRegistry datatypes;
    
    void sample() {
        Date date;
        // ...
        String dateStr = datatypes.getNN(Date.class).format(date, userSessionSource.getLocale());
        // ...
    }
  • Пример форматирования числового значения с 5 знаками после запятой в Web Client:

    com/sample/sales/web/messages_ru.properties
    coordinateFormat = #,##0.00000
    @Inject
    protected Messages messages;
    @Inject
    protected UserSessionSource userSessionSource;
    @Inject
    protected FormatStringsRegistry formatStringsRegistry;
    
    void sample() {
        String coordinateFormat = messages.getMainMessage("coordinateFormat");
        FormatStrings formatStrings = formatStringsRegistry.getFormatStrings(userSessionSource.getLocale());
        NumberFormat format = new DecimalFormat(coordinateFormat, formatStrings.getFormatSymbols());
        String formattedValue = format.format(value);
        // ...
    }
3.2.2.4. Мета-аннотации

Мета-аннотации сущностей - набор пар ключ/значение, содержащих дополнительную информацию о сущностях.

Обращение к мета-аннотациям производится с помощью метода мета-класса getAnnotations().

Источниками мета-аннотаций сущности являются:

  • Аннотации @OnDelete, @OnDeleteInverse, @Extends. При этом в мета-аннотациях создаются служебные объекты связей между сущностями.

  • Расширяемые мета-аннотации, помеченные аннотацией @MetaAnnotation. Эти аннотации конвертируются в мета-аннотации с ключом, соответствующими полному имени класса Java аннотации и значением, являющимся map атрибутов аннотации. Например, аннотация @TrackEditScreenHistory будет иметь значение, являющееся map с единственным элементом: value → true. Платформа предоставляет следующие аннотации такого вида: @NamePattern, @SystemLevel, @EnableRestore, @TrackEditScreenHistory. В вашем приложении или компоненте можно создать собственные аннотации и пометить их аннотацией @MetaAnnotation.

  • Опционально: в файлах metadata.xml также могут быть определены мета-аннотации сущностей. Если мета-аннотация в XML имеет то же имя, что и мета-аннотация, созданная по Java аннотации класса сущности, первая переопределит значение второй.

    Пример переопределения мета-аннотаций в metadata.xml:

    <metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd">
        <!-- ... -->
    
        <annotations>
            <entity class="com.company.customers.entity.Customer">
                <annotation name="com.haulmont.cuba.core.entity.annotation.TrackEditScreenHistory">
                    <attribute name="value" value="true" datatype="boolean"/>
                </annotation>
    
                <property name="name">
                    <annotation name="length" value="200"/>
                </property>
    
                <property name="customerGroup">
                    <annotation name="com.haulmont.cuba.core.entity.annotation.Lookup">
                        <attribute name="type" class="com.haulmont.cuba.core.entity.annotation.LookupType" value="DROPDOWN"/>
                        <attribute name="actions" datatype="string">
                            <value>lookup</value>
                            <value>open</value>
                        </attribute>
                    </annotation>
                </property>
            </entity>
    
            <entity class="com.company.customers.entity.CustomerGroup">
                <annotation name="com.haulmont.cuba.core.entity.annotation.EnableRestore">
                    <attribute name="value" value="false" datatype="boolean"/>
                </annotation>
            </entity>
        </annotations>
    </metadata>

3.2.3. Представления

При извлечении сущностей из базы данных обычно встает вопрос - как обеспечить загрузку связанных сущностей на нужную глубину?

Например, для браузера Заказов нужно отобразить дату и сумму заказа совместно с названием Покупателя, т.е. загрузить связанный экземпляр Покупателя. А для экрана редактирования Заказа необходимо загрузить еще и коллекцию Пунктов заказа, причем каждый Пункт заказа должен содержать связанный экземпляр Товара для отображения его наименования.

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

Другой похожей проблемой является ограничение набора локальных атрибутов сущностей загружаемого графа: например, некоторая сущность имеет 50 атрибутов, в том числе BLOB, а в экране отображается только 10 атрибутов. Зачем загружать из БД, затем сериализовать и передавать клиенту 40 атрибутов, которые ему в данный момент не нужны?

Механизм представлений (views) решает эти проблемы, обеспечивая извлечение из базы данных и передачу клиенту графов сущностей, ограниченных в глубину и по атрибутам. Представление является описателем графа объектов, который требуется в некотором экране UI или другом процессе обработки данных.

Обработка представлений производится следующим образом:

  • Все связи в модели данных объявляются с признаком загрузки по требованию (fetch = FetchType.LAZY, см. Аннотации сущностей).

  • В процессе загрузки данных через DataManager клиентский код помимо JPQL-запроса указывает нужное представление.

  • На основе представления формируется так называемая FetchGroup - особенность лежащего в основе слоя ORM фреймворка EclipseLink. Fetch Group влияет на формирование SQL-запроса к базе данных: как на список возвращаемых полей, так и на соединения с другими таблицами, содержащими связанные сущности.

Независимо от набора атрибутов, определенного в представлении, всегда загружаются следующие атрибуты:

  • id - идентификатор сущности

  • version - для оптимистично блокируемых сущностей, реализующих Versioned

  • deleteTs, deletedBy - для сущностей, реализующих SoftDelete

При попытке прочитать или установить значение незагруженного (не включенного в представление) атрибута генерируется исключение. Проверить, загружен ли некоторый атрибут можно методом EntityStates.isLoaded().

В следующем разделе описываются способы создания представлений.

Ниже приведена информация о внутреннем устройстве механизма представлений.

View
Рисунок 5. Классы представления

Представление определяется экземпляром класса View, в котором:

  • entityClass - класс сущности, для которого определено представление. Другими словами, "корень" дерева загружаемых сущностей.

  • name - имя представления. Должно быть либо null, либо уникальным в пределах данной сущности.

  • properties - коллекция экземпляров класса ViewProperty, соответствующих загружаемым атрибутам сущности.

  • includeSystemProperties - признак включения системных атрибутов (входящих в состав базовых интерфейсов персистентных сущностей BaseEntity и Updatable).

  • loadPartialEntities - признак влияния представления на загрузку локальных (другими словами, непосредственных) атрибутов. Если false, представление влияет только на загрузку ссылочных атрибутов, а локальные атрибуты загружаются всегда, независимо от их присутствия или отсутствия в представлении.

    Данное свойство в некоторой степени контролируется механизмами загрузки данных платформы. См. разделы о загрузке частичных сущностей в DataManager и EntityManager.

Класс ViewProperty имеет следующие свойства:

  • name - имя атрибута сущности

  • view - для ссылочных атрибутов задает представление, с которым необходимо загружать связанную сущность

  • fetch - для ссылочных атрибутов задает способ загрузки связанной сущности из БД. Соответствует перечислению FetchMode:

    • AUTO - платформа автоматически выбирает оптимальный режим в зависимости от типа отношения.

    • UNDEFINED - загрузка будет выполнена по правилам JPA, что означает загрузку отдельными SELECT-запросами.

    • JOIN - загрузка в том же SELECT-запросе путем объединения с таблицей, содержащей ссвязанную сущность.

    • BATCH - загрузка экземпляров связанной сущности будет осуществляться порциями. Подробнее см. здесь.

    Если атрибут fetch не указан, будет использоваться режим AUTO. Если атрибут представляет собой кэшируемую сущность, независимо от указанного значения будет использоваться UNDEFINED.

3.2.3.1. Создание представлений

Представление может быть создано следующими способами:

  • Программно - созданием экземпляра View. Как правило, таким способом создаются представления, используемые в бизнес-логике.

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

    View view = new View(Order.class)
            .addProperty("date")
            .addProperty("amount")
            .addProperty("customer", new View(Customer.class)
                .addProperty("name")
            );

    То же самое можно сделать с помощью ViewBuilder:

    View view = ViewBuilder.of(Order.class)
            .addAll("date", "amount", "customer.name")
            .build();

    ViewBuilder можно также использовать в DataManager через его fluent interface:

    // explicit view builder
    dataManager.load(Order.class)
            .view(viewBuilder ->
                viewBuilder.addAll("date", "amount", "customer.name"))
            .list();
    
    // implicit view builder
    dataManager.load(Order.class)
            .viewProperties("date", "amount", "customer.name")
            .list();
  • Декларативно в экранах - путем объявления внутри XML-дескриптора экрана, см. пример в разделе Декларативное создание компонентов данных. Это рекомендуемый способ при загрузке данных в экранах Generic UI для проектов, использующих CUBA 7.2+.

  • Декларативно в общем репозитории - путем объявления в файле views.xml проекта. Файл views.xml обрабабывается на старте приложения, созданные экземпляры View кэшируются в репозитории представлений ViewRepository. В дальнейшем в любом месте кода приложения требуемое представление можно получить вызовом репозитория с указанием класса сущности и имени представления.

Пример XML-описателя представления, которое должно обеспечить загрузку всех локальных атрибутов сущности Заказ, ассоциированного Покупателя и коллекции Пунктов заказа:

<view class="com.sample.sales.entity.Order"
      name="order-with-customer"
      extends="_local">
    <property name="customer" view="_minimal"/>
    <property name="items" view="item-in-order"/>
</view>
Работа с репозиторием представлений

ViewRepository является бином Spring, доступным для всех блоков приложения. Ссылка на ViewRepository может быть также получена инжекцией или через статические методы класса AppBeans. Для получения экземпляра View, содержащегося в репозитории, используются методы getView().

В репозитории для каждой сущности по умолчанию доступны три представления с именами _local, _minimal и _base:

  • _local включает в себя все локальные атрибуты сущности

  • _minimal включает в себя атрибуты, входящие в имя экземпляра сущности, и задаваемые аннотацией @NamePattern. Если аннотация @NamePattern для сущности не указана, данное представление не включает никаких атрибутов.

  • _base включает в себя все локальные несистемные атрибуты и атрибуты, заданные в аннотации @NamePattern (т.е. фактически _minimal + _local).

CUBA Studio автоматически создает и поддерживает в проекте один файл views.xml. Однако при необходимости можно использовать и несколько таких файлов следующим образом:

  • В модуле global должен находиться файл views.xml со всеми описателями представлений, которые должны быть доступны глобально, т.е. на всех уровнях приложения. Данный файл должен быть зарегистрирован в свойстве приложения cuba.viewsConfig всех блоков, т.е. в файле app.properties модуля core, в файле web-app.properties модуля web и так далее. Это по умолчанию обеспечивает Studio.

  • Если в проекте имеется много общих представлений, их можно разместить в нескольких файлах, например в стандартном views.xml и в дополнительных foo-views.xml, bar-views.xml. Все файлы должны быть зарегистрированы в свойстве cuba.viewsConfig:

    cuba.viewsConfig = +com/company/sales/views.xml com/company/sales/foo-views.xml com/company/sales/bar-views.xml
  • Если существуют представления, которые необходимы только какому-то одному блоку приложения, то можно определить их в аналогичном файле данного блока, например, web-views.xml, и добавить этот файл в свойство cuba.viewsConfig этого блока, т.е. в данном случае в файл web-app.properties.

Если на момент развертывания некоторого представления в репозитории уже есть представление для этого же класса сущности и с таким же именем, то новое будет проигнорировано. Для того чтобы представление заменило имеющееся в репозитории и гарантированно было развернуто, в XML-описателе должен быть явно указан атрибут overwrite = "true".

Рекомендуется давать представлениям "описательные" имена. Например, не "browse", а "customer-browse". Это упрощает поиск XML-описателей представлений по имени в процессе разработки приложения.

3.2.4. Spring-бины

Spring-бины − это классы, созданием экземпляров которых и установкой в них зависимостей управляет контейнер фреймворка Spring. Бины предназначены для реализации бизнес-логики приложения.

Spring Bean представляет собой singleton, то есть в некотором блоке приложения существует только один экземпляр данного класса. Поэтому, если бин содержит изменяемые данные в полях (другими словами, имеет состояние), то обращение к таким данным необходимо синхронизировать.

3.2.4.1. Создание бина

Руководство Create Business Logic in CUBA демонстрирует использование Spring-бина для реализации бизнес-логики.

Для создания Spring-бина достаточно добавить классу Java аннотацию @org.springframework.stereotype.Component. Например:

package com.sample.sales.core;

import com.sample.sales.entity.Order;
import org.springframework.stereotype.Component;

@Component(OrderWorker.NAME)
public class OrderWorker {
    public static final String NAME = "sales_OrderWorker";

    public void calculateTotals(Order order) {
    }
}

Рекомендуется присваивать бину уникальное имя вида {имя_проекта}_{имя_класса}, и определять его в константе NAME.

Аннотация @javax.annotation.ManagedBean также может ипользоваться для определения бина, однако ее наличие может вызывать проблемы при развертывании в некоторые сервера приложений. Поэтому мы рекомендуем использовать только аннотацию @Component из Spring Framework.

Класс Spring-бина должен находиться внутри дерева пакетов с корнем, заданным в элементе context:component-scan файла spring.xml. Для примера выше, файл spring.xml должен содержать элемент:

<context:component-scan base-package="com.sample.sales"/>

что означает, что поиск аннотированных бинов для данного блока приложения будет происходить начиная с пакета com.sample.sales.

Spring-бины можно создавать на любом уровне приложения.

3.2.4.2. Использование бина

Ссылку на бин можно получить с помощью инжекции или класса AppBeans. В качестве примера использования бина рассмотрим реализацию сервиса OrderService, делегирующего выполнение бину OrderWorker:

package com.sample.sales.core;

import com.haulmont.cuba.core.Persistence;
import com.sample.sales.entity.Order;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.inject.Inject;

@Service(OrderService.NAME)
public class OrderServiceBean implements OrderService {

    @Inject
    protected Persistence persistence;

    @Inject
    protected OrderWorker orderWorker;

    @Transactional
    @Override
    public BigDecimal calculateTotals(Order order) {
        Order entity = persistence.getEntityManager().merge(order);
        return orderWorker.calculateTotals(entity);
    }
}

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

3.2.5. JMX-бины

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

JMXBeans

Как видно из диаграммы, JMX-бин состоит из интерфейса и класса реализации. Класс должен представлять собой Spring-бин, то есть иметь аннотацию @Component и уникальное имя. Интерфейс JMX-бина специальным образом регистрируется в spring.xml для создания в текущей JVM собственно JMX-интерфейса.

Вызовы всех методов интерфейса JMX-бина перехватываются с помощью Spring AOP классом−интерцептором MBeanInterceptor, который обеспечивает установку правильного ClassLoader в контексте потока выполнения, и журналирование необработанных исключений.

Интерфейс JMX-бина обязательно должен иметь имя вида {имя_класса}MBean.

С JMX-интерфейсом можно работать из внешних инструментов, таких как jconsole или jvisualvm. Кроме того, в состав блока Web Client платформы входит JMX-консоль, предоставляющая базовые средства просмотра состояния и вызова методов JMX-бинов.

3.2.5.1. Создание JMX-бина

Рассмотрим процесс создания JMX-бина на примере.

  • Интерфейс JMX-бина:

    package com.sample.sales.core;
    
    import org.springframework.jmx.export.annotation.*;
    import com.haulmont.cuba.core.sys.jmx.JmxBean;
    
    @JmxBean(module = "sales", alias = "OrdersMBean")
    @ManagedResource(description = "Performs operations on Orders")
    public interface OrdersMBean {
        @ManagedOperation(description = "Recalculates an order amount")
        @ManagedOperationParameters({@ManagedOperationParameter(name = "orderId", description = "")})
        String calculateTotals(String orderId);
    }
    • Интерфейс и его методы могут содержать аннотации для задания описания JMX-бина и его операций. Это описание будет отображаться во всех инструментах, работающих с данным JMX-интерфейсом, тем самым помогая администратору системы.

    • Необязательная аннотация @JmxBean используется для автоматической регистрации экземпляров класса на сервере JMX в соответствии с атрибутами module и alias. Вы можете зарегистрировать JMX-bean, используя эту аннотацию вместо регистрации в файле spring.xml.

    • Аннотацию @JmxRunAsync можно использовать для указания длительных операций. Если такая операция запускается через встроенную консоль JMX, платформа отображает диалог с неопределенным индикатором прогресса и кнопкой Cancel. Пользователь может прервать операцию и продолжить работу с приложением. Аннотация может также содержать параметр timeout, который устанавливает максимальное время выполнения в миллисекундах, например:

      @JmxRunAsync(timeout = 30000)
      String calculateTotals();

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

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

    • Так как инструменты JMX поддерживают ограниченный набор типов данных, параметры и результат метода желательно задавать типа String, и при необходимости выполнять конвертацию внутри метода. Помимо String, поддерживаются следующие типы параметров: boolean, double, float, int, long, Boolean, Integer.

  • Класс JMX-бина:

    package com.sample.sales.core;
    
    import com.haulmont.cuba.core.*;
    import com.haulmont.cuba.core.app.*;
    import com.sample.sales.entity.Order;
    import org.apache.commons.lang.exception.ExceptionUtils;
    import org.springframework.stereotype.Component;
    import javax.inject.Inject;
    import java.util.UUID;
    
    @Component("sales_OrdersMBean")
    public class Orders implements OrdersMBean {
    
        @Inject
        protected OrderWorker orderWorker;
    
        @Inject
        protected Persistence persistence;
    
        @Authenticated
        @Override
        public String calculateTotals(String orderId) {
            try {
                try (Transaction tx = persistence.createTransaction()) {
                    Order entity = persistence.getEntityManager().find(Order.class, UUID.fromString(orderId));
                    orderWorker.calculateTotals(entity);
                    tx.commit();
                };
                return "Done";
            } catch (Throwable e) {
                return ExceptionUtils.getStackTrace(e);
            }
        }
    }

    Аннотация @Component определяет, что данный класс является Spring-бином с именем sales_OrdersMBean. Имя указано напрямую в аннотации, а не в константе, так как доступ к JMX-бину из кода Java не требуется.

    Рассмотрим реализацию метода calculateTotals().

    • Метод имеет аннотацию @Authenticated, т.е. при входе в метод и при отсутствии в потоке выполнения пользовательской сессии выполняется системная аутентификация.

    • Тело метода обернуто в блок try/catch, так что метод в случае успешного выполнения возвращает строку "Done", а в случае ошибки - stacktrace исключения в виде строки.

      В данном случае все исключения обрабатываются, а значит, не попадают в MBeanInterceptor и не выводятся в журнал автоматически. Поэтому при необходимости логировать исключения здесь нужно добавить вызов логгера в секции catch.

    • Логика метода заключается в том, что он стартует транзакцию, загружает экземпляр сущности Order по идентификатору и передает управление бину OrderWorker для обработки.

  • Если вы не использовали аннотацию @JmxBean при создании интерфейса JMX-бина, вам следует зарегистрировать JMX-бин в файле spring.xml:

    <bean id="sales_MBeanExporter" lazy-init="false"
          class="com.haulmont.cuba.core.sys.jmx.MBeanExporter">
        <property name="beans">
            <map>
                <entry key="${cuba.webContextName}.sales:type=Orders"
                       value-ref="sales_OrdersMBean"/>
            </map>
        </property>
    </bean>

    Все JMX-бины проекта объявляются в одном экземпляре MBeanExporter в элементах map/entry свойства beans. Ключом элемента здесь является JMX ObjectName, значением - имя бина, заданное в аннотации @Component. ObjectName начинается с имени веб-приложения, так как в одном экземпляре сервера приложения (т.е. в одной JVM) может быть развернуто несколько веб-приложений, экспортирующих одинаковые JMX-интерфейсы.

3.2.5.2. JMX-бины платформы

В данном разделе описаны некоторые имеющиеся в платформе JMX-бины.

3.2.5.2.1. CachingFacadeMBean

CachingFacadeMBean предоставляет методы очистки различных кэшей в блоках Middleware и Web Client.

JMX ObjectName: app-core.cuba:type=CachingFacade и app.cuba:type=CachingFacade

3.2.5.2.2. ConfigStorageMBean

ConfigStorageMBean позволяет просматривать и задавать значения свойствам приложения в блоках Middleware, Web Client и Web Portal.

Данный интерфейс имеет отдельные наборы операций для работы с параметрами конфигурации и развертывания (*AppProperties) и с параметрами времени выполнения (*DbProperties). Эти операции отображают только свойства, явно заданные в хранилище. То есть если имеется конфигурационный интерфейс, определяющий некоторое свойство и его значение по умолчанию, но в базе данных или в файлах никакого значения не указано, данные операции не отобразят свойство и его текущее значение.

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

В отличие от операций, описанных выше, операция getConfigValue() всегда возвращает в точности то значение, какое вернул бы соответствующий метод конфигурационного интерфейса, вызванный из кода приложения.

JMX ObjectName:

  • app-core.cuba:type=ConfigStorage

  • app.cuba:type=ConfigStorage

  • app-portal.cuba:type=ConfigStorage

3.2.5.2.3. EmailerMBean

EmailerMBean позволяет просмотреть текущие значения параметров отсылки email, а также отправить тестовое сообщение.

JMX ObjectName: app-core.cuba:type=Emailer

3.2.5.2.4. PersistenceManagerMBean

PersistenceManagerMBean предоставляет следующие возможности:

  • управление механизмом статистики сущностей

  • отображение новых скриптов обновления БД методом findUpdateDatabaseScripts() и запуск обновления методом updateDatabase()

  • запуск произвольных JPQL запросов в контексте Middleware методами jpqlLoadList(), jpqlExecuteUpdate()

JMX ObjectName: app-core.cuba:type=PersistenceManager

3.2.5.2.5. ScriptingManagerMBean

ScriptingManagerMBean является JMX-фасадом для интерфейса инфраструктуры Scripting.

JMX ObjectName: app-core.cuba:type=ScriptingManager

JMX-атрибуты:

JMX-операции:

  • runGroovyScript() - выполнить скрипт Groovy в контексте Middleware и вернуть результат. В скрипт передаются следующие переменные:

    • persistence типа Persistence

    • metadata типа Metadata

    • configuration типа Configuration

    • dataManager типа DataManager

      Для отображения в JMX-интерфейсе результат должен быть типа String. В остальном аналогичен методу Scripting.runGroovyScript().

      Пример скрипта, создающего набор тестовых пользователей:

      import com.haulmont.cuba.core.*
      import com.haulmont.cuba.core.global.*
      import com.haulmont.cuba.security.entity.*
      
      PasswordEncryption passwordEncryption = AppBeans.get(PasswordEncryption.class)
      
      Transaction tx = persistence.createTransaction()
      try {
          EntityManager em = persistence.getEntityManager()
          Group group = em.getReference(Group.class, UUID.fromString('0fa2b1a5-1d68-4d69-9fbd-dff348347f93'))
          for (i in (1..250)) {
              User user = new User()
              user.setGroup(group)
              user.setLogin("user_${i.toString().padLeft(3, '0')}")
              user.setName(user.login)
              user.setPassword(passwordEncryption.getPasswordHash(user.id, '1'));
              em.persist(user)
          }
          tx.commit()
      } finally {
          tx.end()
      }
3.2.5.2.6. ServerInfoMBean

ServerInfoMBean предоставляет общую информацию о данном блоке Middleware: номер и дату сборки, идентификатор сервера.

JMX ObjectName: app-core.cuba:type=ServerInfo

3.2.6. Интерфейсы инфраструктуры

Интерфейсы инфраструктуры обеспечивают доступ к часто используемой функциональности платформы. Большинство из этих интерфейсов расположены в модуле global и могут быть использованы как на среднем слое, так и в блоках клиентского уровня, но некоторые (например, Persistence) доступны только коду среднего слоя.

Интерфейсы инфраструктуры реализуются бинами Spring Framework, поэтому они могут быть инжектированы в любые другие управляемые компоненты (бины, сервисы среднего слоя, контроллеры экранов универсального пользовательского интерфейса).

Кроме того, как и любые другие бины, интерфейсы инфраструктуры могут быть получены с помощью статических методов класса AppBeans и использоваться в неуправляемых компонентах (POJO, вспомогательных классах и пр.).

3.2.6.1. Configuration

Позволяет получать ссылки на конфигурационные интерфейсы там, где невозможна их инжекция.

Пример:

// field injection

@Inject
protected Configuration configuration;
...
String tempDir = configuration.getConfig(GlobalConfig.class).getTempDir();
// setter injection

protected GlobalConfig globalConfig;

@Inject
public void setConfiguration(Configuration configuration) {
    this.globalConfig = configuration.getConfig(GlobalConfig.class);
}
// location

String tempDir = AppBeans.get(Configuration.class).getConfig(GlobalConfig.class).getTempDir();
3.2.6.2. DataManager

Интерфейс DataManager является универсальным средством для загрузки графов сущностей из базы данных, и для сохранения изменений, произведенных в detached экземплярах сущностей.

Руководство Introduction to Working with Data содержит различные варианты использования программного доступа к данным с помощью DataManager API.

В разделе DataManager vs. EntityManager приведена информация о различиях между DataManager и EntityManager.

DataManager на самом деле делегирует выполнение реализациям DataStore, и поддерживает ссылки между сущностями из разных хранилищ. Большинство деталей реализации, описанных ниже, актуальны только когда производится работа через RdbmsStore с сущностями, хранящимися в реляционной БД. Для другого типа хранилища все, кроме сигнатур методов, может отличаться. Для простоты изложения, далее, когда мы говорим просто DataManager, мы будем иметь в виду DataManager через RdbmsStore.

Методы DataManager:

  • load(Class) - загружает сущности указанного типа. Данный метод является точкой входа в fluent API:

    @Inject
    private DataManager dataManager;
    
    private Book loadBookById(UUID bookId) {
        return dataManager.load(Book.class).id(bookId).view("book.edit").one();
    }
    
    private List<BookPublication> loadBookPublications(UUID bookId) {
        return dataManager.load(BookPublication.class)
            .query("select p from library_BookPublication p where p.book.id = :bookId")
            .parameter("bookId", bookId)
            .view("bookPublication.full")
            .list();
    }
  • loadValues(String query) - загружает пары ключ-значение по запросу, возвращающему скалярные значения. Данный метод является точкой входа в fluent API:

    List<KeyValueEntity> list = dataManager.loadValues(
            "select o.customer, sum(o.amount) from demo_Order o " +
            "where o.date >= :date group by o.customer")
        .store("legacy_db") (1)
        .properties("customer", "sum") (2)
        .parameter("date", orderDate)
        .list();
    1 - укажите хранилище, в котором находится сущность. Данный метод можно опустить, если сущность находится в главном хранилище.
    2 - перечислите имена атрибутов результирующей KeyValueEntity. Порядок имен должен соответствовать колонкам результирующего набора в запросе.
  • loadValue(String query, Class valueType) - загружает единственное значение по запросу. Данный метод является точкой входа в fluent API:

    BigDecimal sum = dataManager.loadValue(
            "select sum(o.amount) from demo_Order o " +
            "where o.date >= :date group by o.customer", BigDecimal.class)
        .store("legacy_db") (1)
        .parameter("date", orderDate)
        .one();
    1 - укажите хранилище, в котором находится сущность. Данный метод можно опустить, если сущность находится в главном хранилище.
  • load(LoadContext), loadList(LoadContext) - загружает сущности в соответствии с параметрами переданного объекта LoadContext. В LoadContext обязательно должен быть передан либо JPQL-запрос, либо идентификатор сущности. Если передано и то и другое, используется запрос, а идентификатор игнорируется. Примеры:

    @Inject
    private DataManager dataManager;
    
    private Book loadBookById(UUID bookId) {
        LoadContext<Book> loadContext = LoadContext.create(Book.class)
                .setId(bookId).setView("book.edit");
        return dataManager.load(loadContext);
    }
    
    private List<BookPublication> loadBookPublications(UUID bookId) {
        LoadContext<BookPublication> loadContext = LoadContext.create(BookPublication.class)
                .setQuery(LoadContext.createQuery("select p from library_BookPublication p where p.book.id = :bookId")
                    .setParameter("bookId", bookId))
                .setView("bookPublication.full");
        return dataManager.loadList(loadContext);
    }
  • loadValues(ValueLoadContext) - загружает список пар ключ-значение. Метод принимает объект ValueLoadContext, в котором задается запрос и список ключей. Возвращаемый список содержит экземпляры KeyValueEntity. Например:

    ValueLoadContext context = ValueLoadContext.create()
            .setQuery(ValueLoadContext.createQuery(
                        "select o.customer, sum(o.amount) from demo_Order o " +
                        "where o.date >= :date group by o.customer")
                .setParameter("date", orderDate))
            .addProperty("customer")
            .addProperty("sum");
    List<KeyValueEntity> list = dataManager.loadValues(context);
  • getCount(LoadContext) - возвращает количество записей для запроса, переданного в метод. Когда возможно, для максимальной производительности, стандартная реализация в классе RdbmsStore выполняет запрос select count() с условиями исходного запроса.

  • commit(CommitContext) - сохраняет в базе данных набор сущностей, переданный в объекте CommitContext. Отдельно указываются коллекции сущностей, которые нужно сохранить и которые нужно удалить.

    Метод возвращает набор экземпляров сущностей, возвращенных из метода EntityManager.merge(), то есть по сути свежие экземпляры, только что обновленные в БД. Дальнейшая работа должна производиться именно с этими возвращенными экземплярами, чтобы предотвратить потерю данных или исключения оптимистичной блокировки. Для того, чтобы обеспечить наличие нужных атрибутов у возвращенных сущностей, с помощью мэп CommitContext.getViews() можно указать представление для каждого сохраняемого экземпляра.

    DataManager может выполнять валидацию сохраняемых сущностей.

    Примеры сохранения коллекций сущностей:

    @Inject
    private DataManager dataManager;
    
    private void saveBookInstances(List<BookInstance> toSave, List<BookInstance> toDelete) {
        CommitContext commitContext = new CommitContext(toSave, toDelete);
        dataManager.commit(commitContext);
    }
    
    private Set<Entity> saveAndReturnBookInstances(List<BookInstance> toSave, View view) {
        CommitContext commitContext = new CommitContext();
        for (BookInstance bookInstance : toSave) {
            commitContext.addInstanceToCommit(bookInstance, view);
        }
        return dataManager.commit(commitContext);
    }
  • reload(Entity, View) - удобный метод для перезагрузки экземпляра сущности с требуемым представлением. Делегирует выполнение методу load().

  • remove(Entity) - удаляет экземпляр сущности из базы данных. Делегирует выполнение методу commit().

  • create(Class) - создает экземпляр данной сущности в памяти. Этот метод просто делегирует в Metadata.create().

  • getReference(Class, Object) - возвращает экземпляр сущности, который может быть использован в качестве ссылки на объект, существующий в базе данных.

    Например, если вы создаете экземпляр сущности User, вам необходимо установить ссылку на Group, в которую данный пользователь будет входить. Если вам известен id группы, то вы могли бы загрузить данную группу из БД. Данный метод позволяет получить экземпляр Group без ненужного обращения к БД:

    user.setGroup(dataManager.getReference(Group.class, groupId));
    dataManager.commit(user);

    Ссылка может также быть использована для удаления существующего объекта по идентификатору:

    dataManager.remove(dataManager.getReference(Customer.class, customerId));
Запросы

При работе с реляционными базами данных для загрузки данных используются запросы на JPQL. В разделах Функции JPQL, Поиск подстроки без учета регистра и Макросы в JPQL приведена информация о том, как JPQL в CUBA отличается от стандартного JPA. Кроме того, имейте в виду, что DataManager может выполнять только "select"-запросы.

Метод query() fluent-интерфейса принимает строку запроса как в полном, так и в сокращенном формате. Сокращенный запрос формируется следующим образом:

  • Выражение "select <alias>" всегда можно опустить.

  • Если выражение "from" содержит одну сущность, и вам не нужен особенный алиас, то выражение "from <entity> <alias> where" можно опустить. В этом случае фреймворк будет использовать алиас e.

  • Можно использовать позиционные параметры и передавать их значения прямо в метод query() в дополнительных аргументах.

Например:

// named parameter
dataManager.load(Customer.class)
        .query("e.name like :name")
        .parameter("name", value)
        .list();

// positional parameter
dataManager.load(Customer.class)
        .query("e.name like ?1", value)
        .list();

// case-insensitive positional parameter
dataManager.load(Customer.class)
        .query("e.name like ?1 or e.email like ?1", "(?i)%joe%")
        .list();

// multiple positional parameters
dataManager.load(Order.class)
        .query("e.date between ?1 and ?2", date1, date2)
        .list();

// omitting "select" and using a positional parameter
dataManager.load(Order.class)
        .query("from sales_Order o, sales_OrderLine l " +
                "where l.order = o and l.product.name = ?1", productName)
        .list();

Имейте в виду, что позиционные параметры поддерживаются только в fluent-интерфейсе. В LoadContext.Query можно использовать только именованные параметры.

Транзакции

DataManager всегда стартует новую транзакцию и по завершении работы выполняет коммит, таким образом возвращая сущности в detached состоянии. На среднем слое можно использовать TransactionalDataManager, если необходимо реализовать сложное транзакционное поведение.

Частичные сущности

Частичная сущность - это экземпляр сущности, в котором может быть загружена только часть локальных атрибутов. По умолчанию, DataManager загружает частичные сущности в соответствии с указанными представлениями. (на самом деле, RdbmsStore просто устанавливает свойство loadPartialEntities у представления в true и передает его дальше в EntityManager).

В некоторых случаях DataManager загружает все локальные атрибуты и представление определяет только загрузку связей:

3.2.6.2.1. DataManager vs. EntityManager

И DataManager и EntityManager предназначены для выполнения операций с сущностями (CRUD). Ниже приведены различия между этими интерфейсами.

DataManager EntityManager

DataManager доступен и на среднем слое и на клиентском уровне.

EntityManager доступен только на среднем слое.

DataManager является синглтон-бином.

Ссылку на EntityManager необходимо получать через интерфейс Persistence.

DataManager содержит несколько высокоуровневых методов для работы с detached сущностями: load(), loadList(), reload(), commit().

EntityManager в большой степени повторяет стандартный javax.persistence.EntityManager.

DataManager может выполнять bean validation при сохранении сущностей.

EntityManager не выполняет bean validation.

DataManager на самом деле делегирует выполнение реализациям DataStore, поэтому особенности DataManager, перечисленные ниже, актуальны только для наиболее часто встречающегося случая, когда вы работаете с сущностями, хранящимися в реляционной базе данных.

DataManager EntityManager

DataManager всегда стартует новую транзакцию внутри. На среднем слое можно использовать TransactionalDataManager, если необходимо реализовать сложное транзакционное поведение.

Для работы с EntityManager необходима открытая транзакция.

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

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

DataManager выполняет только JPQL запросы. Кроме того, он имеет отдельные методы для загрузки сущностей: load(), loadList(); и скалярных и агрегатных значений: loadValues().

EntityManager может выполнять любые JPQL или native (SQL) запросы.

DataManager проверяет права доступа, когда вызывается с клиентского уровня.

EntityManager не проверяет права доступа.

При работе на клиентском уровне доступен только DataManager. На среднем слое, используйте TransactionalDataManager когда необходимо реализовать атомарную логику внутри транзакции, или EntityManager если он лучше подходит для решения задачи. Вообще, на среднем слое можно использовать любой из этих интерфейсов.

Если вам нужно обойти ограничения DataManager при работе на клиентском уровне, создайте свой сервис и используйте TransactionalDataManager или EntityManager для работы с данными. В сервисе можно проверять права пользователя с помощью интерфейса Security и возвращать клиенту данные в виде персистентных или неперсистентных сущностей или произвольных значений.

3.2.6.2.2. TransactionalDataManager

TransactionalDataManager - это бин среднего слоя, который во многом повторяет интерфейс DataManager, но при этом может присоединяться к существующей транзакции. Он имеет следуюшие особенности:

  • При наличии активной транзакции, выполняет действия в ее контексте, в противном случае стартует и коммитит новую транзакцию так же как DataManager.

  • Принимает и возвращает сущности в состоянии detached. Разработчик должен загружать сущности с необходимыми представлениями и явно вызывать метод save() для сохранения измененных экземпляров в БД.

  • Применяет ограничения row-level security, работает с динамическими атрибутами и ссылками между хранилищами так же как DataManager.

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

@Inject
private TransactionalDataManager txDataManager;

@Transactional
public void transfer(Id<Account, UUID> acc1Id, Id<Account, UUID> acc2Id, Long amount) {
    Account acc1 = txDataManager.load(acc1Id).one();
    Account acc2 = txDataManager.load(acc2Id).one();
    acc1.setBalance(acc1.getBalance() - amount);
    acc2.setBalance(acc2.getBalance() + amount);
    txDataManager.save(acc1);
    txDataManager.save(acc2);
}

Более сложный пример можно найти в тесте фреймворка: DataManagerTransactionalUsageTest.java

TransactionalDataManager особенно полезен при обработке события EntityChangedEvent в текущей транзакции. Он позволяет получить текущее состояние измененной сущности из базы данных до коммита транзакции.

3.2.6.2.3. Права доступа в DataManager

Методы load(), loadList(), loadValues() и getCount() проверяют наличие у пользователя права READ на загружаемую сущность. Кроме того, при извлечении сущностей из БД накладываются ограничения групп доступа.

Метод commit() проверяет наличие у пользователя права UPDATE на изменяемые сущности и DELETE на удаляемые.

По умолчанию, DataManager проверяет права на операции (READ/CREATE/UPDATE/DELETE) с сущностями, когда вызывается с клиентской стороны, и игнорирует их, когда вызывается из кода middleware. Права на атрибуты по умолчанию не проверяются.

Если вы хотите, чтобы DataManager проверял права на операции и при вызове на среднем слое, получите методом DataManager.secure() специальный объект-обертку и вызывайте методы у него. В качестве альтернативы, вы можете установить свойство приложения cuba.dataManagerChecksSecurityOnMiddleware, чтобы проверка прав работала для всего приложения.

Права на атрибуты будут проверяться на среднем слое только если вы дополнительно установите свойство приложения cuba.entityAttributePermissionChecking в true. Это имеет смысл, если средний слой обслуживает также клиентов, которые теоретически могут быть взломаны, например, десктоп-клиент. В этом случае, установите также свойство cuba.keyForSecurityTokenEncryption в уникальное значение. Если ваше приложение использует только Web или Portal клиентов, то оба данных свойства можно безопасно оставить со значениями по умолчанию.

Имейте в ввиду, что ограничения групп доступа (row-level security) применяются всегда, независимо от того, был ли вызов с клиентского или со среднего слоя.

См. раздел Проверки доступа к данным для получения полной картины того, как разрешения и ограничения доступа к данным используются различными механизмами фреймворка.

3.2.6.2.4. Запросы с distinct

В JPQL запросах для экранов со списками сущностей, в которых включено постраничное отображение и возможна непредсказуемая модификация запроса универсальным фильтром или механизмом ограничений групп доступа, при отсутствии в запросе оператора distinct может возникать следующий эффект:

  • при объединении с коллекцией на уровне извлечения из базы данных возникает набор с дубликатами строк

  • на клиентском уровне в источнике данных дубликаты исчезают, т.к. попадают в мэп (java.util.Map)

  • при постраничном отображении на одной странице оказывается меньшее количество строк, чем запрошено, общее количество строк наоборот завышено.

Таким образом, рекомендуется в JPQL запросы браузеров включать предложение distinct, которое гарантирует отсутствие дубликатов записей при выборке из базы данных. Однако в некоторых серверах БД (в частности PostgreSQL) при большом количестве извлекаемых записей (более 10000) SQL запрос с distinct выполняется недопустимо долго.

Для решения этой проблемы в платформе реализована возможность корректной работы без distinct на уровне SQL. Данный механизм включается свойством приложения cuba.inMemoryDistinct, при активации которого выполняется следующее:

  • В JPQL запросе должен по-прежнему присутствовать select distinct

  • В DataManager из JPQL запроса перед отправкой в ORM distinct вырезается

  • После загрузки страницы данных на Middleware удаляются дубликаты и выполняются дополнительные запросы к БД для получения нужного количества строк, которые затем и возвращаются клиенту.

3.2.6.2.5. Последовательная выборка

DataManager может выполнять последовательную выборку данных из результатов предыдущего запроса. Эта возможность используется в универсальном фильтре при последовательном наложении фильтров.

Данный механизм работает следующим образом:

  • При получении LoadContext с установленными атрибутами prevQueries и queryKey DataManager выполняет выборку по предыдущему запросу и сохраняет идентификаторы полученных сущностей в таблице SYS_QUERY_RESULT (соответствующей сущности sys$QueryResult), разделяя наборы записей по идентификаторам пользовательских сессий и ключу сеанса выборки queryKey.

  • Текущий запрос модифицируется для объединения с результатами предыдущего, так что в итоге возвращает данные, соответствующие условиям обоих запросов, объединенных по "И".

  • Далее процесс может повторяться, при этом уменьшающийся набор предыдущих результатов удаляется из таблицы SYS_QUERY_RESULT и заполняется заново.

Таблица SYS_QUERY_RESULT периодически очищается от ненужных результатов запросов, оставленных завершенными пользовательскими сессиями. Для этого предназначен метод deleteForInactiveSessions бина QueryResultsManagerAPI, который вызывается шедулером Spring, объявленным в cuba-spring.xml. По умолчанию это происходит раз в 10 минут, но вы можете задать другой интервал в миллисекундах, используя свойство приложения cuba.deleteOldQueryResultsInterval в модуле core.

3.2.6.3. EntityStates

Интерфейс для получения информации о персистентных сущностях, сохраняемых через ORM. В отличие от бинов Persistence и PersistenceTools доступен на всех уровнях приложения.

Методы EntityStates:

  • isNew() - определяет, является ли переданный экземпляр только что созданным. Он может быть либо в состоянии New, либо Managed, но только что сохраненным в текущей транзакции. Также возвращает true для неперсистентных сущностей.

  • isManaged() - определяет, находится ли переданный экземпляр в состоянии Managed, то есть присоединен к персистентному контексту.

  • isDetached() - определяет, находится ли переданный экземпляр в состоянии Detached. Возвращает true, также если экземпляр не является персистентной сущностью.

  • isLoaded() - определяет, загружен ли данный атрибут сущности. Атрибут загружается, если он включен в представление, или если это локальный атрибут и никакое представление не использовалось в процессе загрузки через EntityManager или DataManager. Данный метод поддерживает проверку только непосредственных атрибутов сущностей.

  • checkLoaded() - то же самое что и isLoaded(), но выбрасывает IllegalArgumentException если хотя бы один из переданных атрибутов не загружен.

  • isLoadedWithView() - принимает экземпляр сущности и представление, и возвращает true если все атрибуты требуемые в представлении на самом деле загружены.

  • checkLoadedWithView() - то же самое что и isLoadedWithView(), но выбрасывает IllegalArgumentException вместо возврата false.

  • makeDetached() - принимает только что созданный экземпляр сущности и переводит его в состояние detached. Detached-объект можно передать в DataManager.commit() или EntityManager.merge() для сохранения его состояния в базе данных. Подробнее см. API docs.

  • makePatch() - принимает только что созданный экземпляр сущности и превращает его в patch-объект. Patch-объект можно передать в DataManager.commit() или EntityManager.merge() для сохранения его состояния в базе данных, при этом в отличие от detached-объекта, будут сохранены только не-null атрибуты. Подробнее см. API docs.

3.2.6.3.1. PersistenceHelper

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

3.2.6.4. Events

Руководство Decouple Business Logic with Application Events содержит примеры использования событий в приложениях.

Бин Events реализует функциональность публикации объектов-событий уровня приложения. События могут использоваться для передачи данных между слабо связанными компонентами приложения. Бин Events является простым фасадом для объекта ApplicationEventPublisher Spring Framework.

public interface Events {
    String NAME = "cuba_Events";

    void publish(ApplicationEvent event);
}

Этот бин имеет только один метод - publish(), принимающий объект события. Метод Events.publish() уведомляет все слушатели, зарегистрированные в приложении и подписанные на события того же типа, что и переданный объект. Вы может использовать класс-обёртку PayloadApplicationEvent для публикации любых объектов в качестве событий.

Обработка событий в компонентах приложения

Прежде всего, необходимо создать класс события. Он должен быть наследником класса ApplicationEvent. Класс события может включать любые дополнительные данные. Например:

package com.company.sales.core;

import com.haulmont.cuba.security.entity.User;
import org.springframework.context.ApplicationEvent;

public class DemoEvent extends ApplicationEvent {

    private User user;

    public DemoEvent(Object source, User user) {
        super(source);
        this.user = user;
    }

    public User getUser() {
        return user;
    }
}

Бины могут публиковать события, используя бин Events:

package com.company.sales.core;

import com.haulmont.cuba.core.global.Events;
import com.haulmont.cuba.core.global.UserSessionSource;
import com.haulmont.cuba.security.global.UserSession;
import org.springframework.stereotype.Component;
import javax.inject.Inject;

@Component
public class DemoBean {

    @Inject
    private Events events;
    @Inject
    private UserSessionSource userSessionSource;

    public void demo() {
        UserSession userSession = userSessionSource.getUserSession();
        events.publish(new DemoEvent(this, userSession.getUser()));
    }
}

По умолчанию все события обрабатываются синхронно.

Есть два способа обработки событий:

  • Реализовать интерфейс ApplicationListener.

  • Использовать аннотацию @EventListener для метода.

В первом случае, мы должны создать бин, реализующий интерфейс ApplicationListener с указанием типа события:

@Component
public class DemoEventListener implements ApplicationListener<DemoEvent> {
    @Inject
    private Logger log;

    @Override
    public void onApplicationEvent(DemoEvent event) {
        log.debug("Demo event is published");
    }
}

Второй способ может использоваться для сокрытия деталей реализации обработчика событий и обработки множества различных событий в одном бине:

@Component
public class MultipleEventListener {
    @Order(10)
    @EventListener
    protected void handleDemoEvent(DemoEvent event) {
        // handle event
    }

    @Order(1010)
    @EventListener
    protected void handleUserLoginEvent(UserLoggedInEvent event) {
        // handle event
    }
}

По умолчанию, события в Spring требуют модификаторов доступа protected, package или public для методов, аннотированных @EventListener. Обратите внимание, что модификатор private не поддерживается.

Методы, аннотированные @EventListener, не работают в services, JMX-бинах и других бинах с интерфейсами. При использовании @EventListener в таком бине на старте приложения будет выброшено исключение:

BeanInitializationException: Failed to process @EventListener annotation on bean. Need to invoke method declared on target class, but not found in any interface(s) of the exposed proxy type. Either pull the method up to an interface or switch to CGLIB proxies by enforcing proxy-target-class mode in your configuration.

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

Вы можете использовать интерфейс Ordered и аннотацию @Order Spring Framework для указания порядка исполнения обработчиков событий. Все бины и обработчики событий платформы используют значение order от 100 до 1000, таким образом, вы можете добавить обработчик событий как до, так и после обработчиков события платформы. Если вы хотите добавить свой обработчик события до обработчиков из платформы, то используйте значение меньше 100.

См. также События логина.

Обработка событий в экранах

Обычно, бин Events делегирует публикацию события объекту ApplicationContext. Для блока Web Client это поведение отличается от стандартного, вы можете использовать дополнительный интерфейс для классов событий - UiEvent. Это интерфейс-маркер для событий, которые должны быть доставлены в экраны пользовательского интерфейса текущего экземпляра UI (текущей вкладки веб-браузера).

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

Пример класса события:

package com.company.sales.web;

import com.haulmont.cuba.gui.events.UiEvent;
import com.haulmont.cuba.security.entity.User;
import org.springframework.context.ApplicationEvent;

public class UserRemovedEvent extends ApplicationEvent implements UiEvent {

    private User user;

    public UserRemovedEvent(Object source, User user) {
        super(source);
        this.user = user;
    }

    public User getUser() {
        return user;
    }
}

События публикуются при помощи бина Events из контроллера экрана так же, как и из бинов Spring:

@Inject
Events events;
// ...
UserRemovedEvent event = new UserRemovedEvent(this, removedUser);
events.publish(event);

Чтобы обработать событие, вы должны объявить в экране метод с аннотацией @EventListener (Интерфейс ApplicationListener не поддерживается):

@Order(15)
@EventListener
protected void onUserRemove(UserRemovedEvent event) {
    notifications.create()
            .withCaption("User is removed " + event.getUser())
            .show();
}

Вы можете использовать аннотацию @Order, чтобы задать порядок вызова обработчиков события.

Если класс события реализует UiEvent, и объект такого события опубликован при помощи бина Events из потока UI, то будут вызваны обработчики событий этого типа в открытых на данный момент окнах и фреймах. Обработка событий синхронная. Только экраны и фреймы текущей активной вкладки веб-браузера получат уведомление о событии.

3.2.6.5. Messages

Интерфейс Messages обеспечивает получение локализованных строк сообщений.

Рассмотрим методы интерфейса подробнее.

  • getMessage() - возвращает локализованное сообщение по ключу, имени пакета сообщений и требуемой локали. Существует несколько модификаций данного метода в зависимости от набора параметров. Если локаль не указана в параметре метода, используется локаль текущего пользователя.

    Примеры:

    @Inject
    protected Messages messages;
    ...
    String message1 = messages.getMessage(getClass(), "someMessage");
    String message2 = messages.getMessage("com.abc.sales.web.customer", "someMessage");
    String message3 = messages.getMessage(RoleType.STANDARD);
  • formatMessage() - находит локализованное сообщение по ключу, имени пакета сообщений и требуемой локали, и использует его для форматирования переданных параметров. Формат задается по правилам метода String.format(). Существует несколько модификаций данного метода в зависимости от набора параметров. Если локаль не указана в параметре метода, используется локаль текущего пользователя.

    Пример:

    String formattedValue = messages.formatMessage(getClass(), "someFormat", someValue);
  • getMainMessage() - возвращает локализованное сообщение из главного пакета данного блока приложения.

    Пример:

    protected Messages messages = AppBeans.get(Messages.class);
    ...
    messages.getMainMessage("actions.Ok");
  • getMainMessagePack() - возвращает имя главного пакета сообщений данного блока приложения.

    Пример:

    String formattedValue = messages.formatMessage(messages.getMainMessagePack(), "someFormat", someValue);
  • getTools() - возвращает экземпляр интерфейса MessageTools (см. ниже).

3.2.6.5.1. MessageTools

ManagedBean, содержащий вспомогательные методы работы с локализованными сообщениями. Интерфейс MessageTools можно получить либо методом Messages.getTools(), либо как любой другой бин - инжекцией или через класс AppBeans.

Методы MessageTools:

  • loadString() - возвращает локализованное сообщение, заданное ссылкой вида msg://{messagePack}/{key}.

    Составные части ссылки:

    • msg:// - обязательный префикс.

    • {messagePack} - необязательное имя пакета сообщения. Если не указано, предполагается, что имя пакета передается в loadString() отдельным параметром.

    • {key} - ключ сообщения в пакете.

      Примеры ссылок на сообщения:

      msg://someMessage
      msg://com.abc.sales.web.customer/someMessage
  • getEntityCaption() - возвращает локализованное название сущности.

  • getPropertyCaption() - возвращает локализованное название атрибута сущности.

  • hasPropertyCaption() - определяет, задано ли для атрибута сущности локализованное название.

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

  • getDefaultLocale() - возвращает локаль приложения по умолчанию, то есть указанную первой в списке свойства cuba.availableLocales.

  • useLocaleLanguageOnly() - возвращает true, если в списке поддерживаемых приложением локалей, заданном свойством cuba.availableLocales, для всех локалей определен только язык, а country и variant не указаны. Этим методом пользуются механизмы платформы, которым необходимо найти наиболее подходящую локаль из списка поддерживаемых на основе локали, полученной из внешних источников, таких как операционная система или HTTP запрос.

  • trimLocale() - удаляет из переданной локали все кроме языка, если метод useLocaleLanguageOnly() возвращает true.

Для расширения набора вспомогательных методов в конкретном приложении бин MessageTools можно переопределить. Примеры работы с расширенным интерфейсом:

MyMessageTools tools = messages.getTools();
tools.foo();
((MyMessageTools) messages.getTools()).foo();
3.2.6.6. Metadata

Интерфейс Metadata обеспечивает доступ к сессии метаданных и репозиторию представлений.

Методы интерфейса:

  • getSession() - возвращает экземпляр сессии метаданных

  • getViewRepository() - возвращает экземпляр репозитория представлений

  • getExtendedEntities() - возвращает экземпляр ExtendedEntities, предназначенный для работы с расширенными сущностями. Подробнее см. Расширение сущности

  • create() - создать экземпляр сущности, учитывая возможность расширения.

    Для персистентных наследников BaseLongIdEntity и BaseIntegerIdEntity данный метод также присваивает идентификаторы. Значения идентификаторов получаются из автоматически создаваемых в базе данных последовательностей. По умолчанию последовательности создаются в основном хранилище. Если же свойство приложения cuba.useEntityDataStoreForIdSequence установлено в true, последовательности будут создаваться в хранилище, к которому принадлежит данная сущность.

  • getTools() - возвращает экземпляр интерфейса MetadataTools (см. ниже).

3.2.6.6.1. MetadataTools

ManagedBean, содержащий вспомогательные методы работы с метаданными. Интерфейс MetadataTools можно получить либо методом Metadata.getTools(), либо как любой другой бин - инжекцией или через класс AppBeans.

Методы MetadataTools:

  • getAllPersistentMetaClasses() - возвращает коллекцию мета-классов персистентных сущностей

  • getAllEmbeddableMetaClasses() - возвращает коллекцию мета-классов встраиваемых сущностей

  • getAllEnums() - возвращает коллекцию классов перечислений, используемых в качестве типов атрибутов сущностей

  • format() - форматирует переданное значение в соответствии с типом данных заданного мета-свойства

  • isSystem() - определяет, является ли переданное мета-свойство системным, т.е. заданным в одном из базовых интерфейсов сущностей

  • isPersistent() - определяет, является ли переданное мета-свойство персистентным, т.е. хранимым в БД

  • isTransient() - определяет, является ли переданное мета-свойство или произвольный атрибут неперсистентным

  • isEmbedded() - определяет, является ли переданное мета-свойство встроенным объектом

  • isAnnotationPresent() - определяет наличие указанной аннотации на классе или его предках

  • getNamePatternProperties() - возвращает коллекцию мета-свойств атрибутов, входящих в имя экземпляра, возвращаемого методом Instance.getInstanceName(). См. @NamePattern.

Для расширения набора вспомогательных методов в конкретном приложении бин MetadataTools можно переопределить. Примеры работы с расширенным интерфейсом:

MyMetadataTools tools = metadata.getTools();
tools.foo();
((MyMetadataTools) metadata.getTools()).foo();
3.2.6.7. Resources

Обеспечивает загрузку ресурсов по следующим правилам:

  1. если указанное местонахождение представляет собой URL, ресурс загружается из этого URL;

  2. если указанное местонахождение начинается с префикса classpath:, ресурс загружается из classpath;

  3. если не URL и не начинается с classpath:, то:

    1. в каталоге конфигурации приложения ищется файл, используя указанное местонахождение как относительный путь. Если файл найден, ресурс загружается из него;

    2. если ресурс не найден на предыдущих этапах, он загружается из classpath.

На практике явное указание URL или префикса classpath: используется редко, т.е. обычно ресурсы загружаются либо из конфигурационного каталога, либо из classpath. Ресурс в конфигурационном каталоге замещает одноименный ресурс в classpath.

Методы Resources:

  • getResourceAsStream() - возвращает InputStream для указанного ресурса, либо null, если ресурс не найден. Поток должен быть закрыт после использования, например:

    @Inject
    protected Resources resources;
    ...
    InputStream stream = null;
    try {
        stream = resources.getResourceAsStream(resourceLocation);
        ...
    } finally {
        IOUtils.closeQuietly(stream);
    }

    Возможно использование "try with resources":

    try (InputStream stream = resources.getResourceAsStream(resourceLocation)) {
        ...
    }
  • getResourceAsString() - возвращает указанный ресурс в виде строки, либо null, если ресурс не найден

3.2.6.8. Scripting

Интерфейс Scripting позволяет динамически (т.е. во время работы приложения) компилировать и загружать классы Java и Groovy, а также выполнять скрипты и выражения на Groovy.

Методы Scripting:

  • evaluateGroovy() - выполняет выражение на Groovy и возвращает его результат.

    Свойство приложения cuba.groovyEvaluatorImport позволяет определить общий набор импортируемых классов, подставляемых в каждое выполняемое выражение. По умолчанию все стандартные блоки приложения импортируют класс PersistenceHelper.

    Скомпилированные выражения кэшируются, что значительно ускоряет повторное выполнение.

    Пример:

    @Inject
    protected Scripting scripting;
    ...
    Integer intResult = scripting.evaluateGroovy("2 + 2", new Binding());
    
    Binding binding = new Binding();
    binding.setVariable("instance", new User());
    Boolean boolResult = scripting.evaluateGroovy("return PersistenceHelper.isNew(instance)", binding);
  • runGroovyScript() - выполняет скрипт Groovy и возвращает его результат.

    Скрипт должен быть расположен либо в конфигурационном каталоге приложения, либо в classpath (текущая реализация Scripting поддерживает ресурсы classpath только внутри JAR-файлов). Скрипт в конфигурационном каталоге замещает одноименный скрипт в classpath.

    Путь к скрипту указывается с разделителями /, в начале пути символ / не требуется.

    Пример:

    @Inject
    protected Scripting scripting;
    ...
    Binding binding = new Binding();
    binding.setVariable("itemId", itemId);
    BigDecimal amount = scripting.runGroovyScript("com/abc/sales/CalculatePrice.groovy", binding);
  • loadClass() - загружает Java или Groovy класс, используя следующую последовательность действий:

    1. Если класс уже загружен, возвращает его.

    2. Ищет исходный текст Groovy (файл *.groovy) в каталоге конфигурации. Если найден, компилирует его, загружает и возвращает класс.

    3. Ищет исходный текст Java (файл *.java) в каталоге конфигурации. Если найден, компилирует его, загружает и возвращает класс.

    4. Ищет скомпилированный класс в classpath, если найден - загружает и возвращает его.

    5. Если ничего не найдено, возвращает null.

      Файлы исходных текстов Java и Groovy в каталоге конфигурации можно изменять во время работы приложения. При следующем вызове loadClass() соответствующий класс будет перекомпилирован и возвращен новый, однако существуют следующие ограничения:

      • нельзя изменять тип исходного текста с Groovy на Java

      • если существовал исходный текст Groovy, и был однажды скомпилирован, то удаление файла исходного текста не приведет к загрузке другого класса из classpath - будет по-прежнему возвращаться класс, скомпилированный из удаленного исходника.

        Пример:

        @Inject
        protected Scripting scripting;
        ...
        Class calculatorClass = scripting.loadClass("com.abc.sales.PriceCalculator");
  • getClassLoader() - возвращает ClassLoader, способный работать по правилам, описанным выше для метода loadClass().

Кэш скомпилированных классов можно очистить во время выполнения с помощью JMX-бина CachingFacadeMBean.

См. также ScriptingManagerMBean.

3.2.6.9. Security

Обеспечивает авторизацию - проверку прав пользователя на различные объекты системы. Перед вызовом соответствующих методов UserSession выполняется поиск исходного мета-класса сущности, что является важным при наличии расширений. Кроме методов, дублирующих методы UserSession, данный интерфейс имеет методы isEntityAttrReadPermitted() и isEntityAttrUpdatePermitted(), предназначенные для определения доступности пути к атрибуту с учетом доступности атрибутов и сущностей, входящих в этот путь.

Интерфейс Security рекомендуется использовать в прикладном коде вместо вызовов методов UserSession.isXYXPermitted().

3.2.6.10. TimeSource

Обеспечивает получение текущего времени. Применение new Date() и т.п. в прикладном коде не рекомендуется.

Примеры:

@Inject
protected TimeSource timeSource;
...
Date date = timeSource.currentTimestamp();
long startTime = AppBeans.get(TimeSource.class).currentTimeMillis();
3.2.6.11. UserSessionSource

Обеспечивает получение объекта сессии текущего пользователя. Подробнее см. Аутентификация пользователей.

3.2.6.12. UuidSource

Обеспечивает получение значений UUID, в том числе для идентификаторов сущностей. Применение UUID.randomUUID() в прикладном коде не рекомендуется.

Для вызова из статического контекста можно использовать класс UuidProvider, который имеет также дополнительный метод fromString(), работающий быстрее, чем стандартный метод UUID.fromString().

3.2.7. AppContext

AppContext - системный класс, в статических полях которого хранятся ссылки на некоторые общие для любого блока приложения компоненты:

  • ApplicationContext фреймворка Spring

  • Набор свойств приложения, загруженных из файлов app.properties

  • ThreadLocal переменная, хранящая экземпляры SecurityContext

  • Коллекция слушателей жизненного цикла приложения (AppContext.Listener)

AppContext инициализируется на запуске приложения классами-загрузчиками, специфичными для типа блока приложения:

  • загрузчик Middleware - AppContextLoader

  • загрузчик Web Client - WebAppContextLoader

  • загрузчик Web Portal - PortalAppContextLoader

AppContext может быть использован в прикладном коде для решения следующих задач:

  • Получения значений свойств приложения, хранимых в файлах app.properties, если они недоступны через конфигурационные интерфейсы.

  • Передачи SecurityContext в новые потоки выполнения, см. Аутентификация пользователей.

  • Регистрации слушателей, срабатывающих после полной инициализации и перед закрытием приложения, например:

    AppContext.addListener(new AppContext.Listener() {
        @Override
        public void applicationStarted() {
            System.out.println("Application is ready");
        }
    
        @Override
        public void applicationStopped() {
            System.out.println("Application is closing");
        }
    });

    Рекомендуемый способ выполнения кода в момент запуска и остановки приложения - это использование События жизненного цикла.

3.2.8. События жизненного цикла

В приложении на CUBA существуют следующие типы событий жизненного цикла:

AppContextInitializedEvent

Посылается сразу после инициализации AppContext. В этот момент:

  • Полностью инициализированы все бины, в том числе выполнены их методы @PostConstruct.

  • Можно использовать статические методы получения бинов AppBeans.get().

  • Метод AppContext.isStarted() возвращает false.

  • Метод AppContext.isReady() возвращает false.

AppContextStartedEvent

Посылается после AppContextInitializedEvent и после запуска всех AppContext.Listener.applicationStarted(). В этот момент:

  • Метод AppContext.isStarted() возвращает true.

  • Метод AppContext.isReady() возвращает false.

  • В блоке Middleware: если свойство приложения cuba.automaticDatabaseUpdate включено, все скрипты обновления БД успешно выполнены.

AppContextStoppedEvent

Посылается перед остановкой приложения и после запуска всех AppContext.Listener.applicationStopped(). В этот момент:

  • Все бины работоспособны и доступны через статические методы AppBeans.get().

  • Метод AppContext.isStarted() возвращает false.

  • Метод AppContext.isReady() возвращает false.

Порядком исполнения слушателей можно управлять с помощью аннотации @Order. Константы Events.HIGHEST_PLATFORM_PRECEDENCE и Events.LOWEST_PLATFORM_PRECEDENCE определяют диапазон значений, используемый слушателями платформы.

Пример:

package com.company.demo.core;

import com.haulmont.cuba.core.global.Events;
import com.haulmont.cuba.core.sys.events.*;
import org.slf4j.Logger;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import javax.inject.Inject;

@Component
public class MyAppLifecycleBean {

    @Inject
    private Logger log;

    // event type is defined by annotation parameter
    @EventListener(AppContextInitializedEvent.class)
    // run after all platform listeners
    @Order(Events.LOWEST_PLATFORM_PRECEDENCE + 100)
    protected void appInitialized() {
        log.info("Initialized");
    }

    // event type is defined by method parameter
    @EventListener
    protected void appStarted(AppContextStartedEvent event) {
        log.info("Started");
    }

    @EventListener
    protected void appStopped(AppContextStoppedEvent event) {
        log.info("Stopped");
    }
}
ServletContextInitializedEvent

Посылается сразу после инициализации контекстов ServletContext и AppContext. В этот момент:

  • Можно использовать статические методы получения бинов AppBeans.get().

  • Событие содержит в себе контексты, позволяющие зарегистрировать собственные сервлеты, фильтры и слушатели, см. раздел Регистрация сервлетов и фильтров.

ServletContextDestroyedEvent

Посылается перед уничтожением контекстов ServletContext и AppContext и позволяет вручную освободить ресурсы.

Пример использования:

@Component
public class MyInitializerBean {

    @Inject
    private Logger log;

    @EventListener
    public void foo(ServletContextInitializedEvent e) {
        log.info("Application and servlet context is initialized");
    }

    @EventListener
    public void bar(ServletContextDestroyedEvent e) {
        log.info("Application is about to shut down, all contexts are now destroyed");
    }
}

3.2.9. Свойства приложения

Свойства приложения − именованные значения различных типов, определяющие всевозможные аспекты конфигурации и функционирования приложения. Свойства приложения широко используются в платформе, и могут применяться в приложении для решения аналогичных задач.

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

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

    Например: cuba.springContextConfig.

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

  • Параметры времени выполнения - активность аудита, параметры отсылки email и т.д. Параметры времени выполнения могут быть изменены при необходимости во время работы приложения без его перезапуска.

Задание свойств приложения

Значения свойств приложения могут быть заданы в базе данных, в файлах свойств, через системные свойства Java или переменные окружения ОС. Если свойство с некоторым именем задано в нескольких источниках, его значение определяется в следующем приоритете:

  1. Системное свойство Java (высший приоритет)

  2. Переменная окружения ОС

  3. Файл свойств

  4. База данных (низший приоритет)

Например, значение, заданное в файле, переопределяет одноименное значение, заданное в БД.

Для переменных окружения ОС фреймворк пытается сначала найти точное соответствие по имени свойства, и если такой переменной нет, то ищется переменная с именем в верхнем регистре и с подчеркиваниями вместо точек. Например, переменная окружения MYAPP_SOMEPROPERTY может задать значение свойству myapp.someProperty. Для того, чтобы запретить возможность использования имен в верхнем регистре с подчеркиваниями, установите свойство приложения cuba.disableUppercaseEnvironmentProperties в true.

Некоторые свойства не поддерживают установку свойств в базе данных по причине того, что их значения требуются еще до того, как БД становится доступной приложению. Это параметры конфигурации и развертывания. Поэтому их можно устанавливать только в файлах свойств, через системные свойства Java или переменные окружения ОС. Параметры времени выполнения всегда могут быть установлены в базе данных (и, возможно, переопределены в файле или системными свойствами).

Как правило, некоторое свойство используется только в одном или нескольких блоках приложения. Например, cuba.persistenceConfig необходимо только для Middleware, cuba.web.appWindowMode − только для Web Client, а cuba.springContextConfig − для всех блоков. Это означает, что если нужно задать значение некоторому свойству, это необходимо сделать во всех блоках, в которых данное свойство используется. Свойства, хранящиеся в БД, доступны всем блокам, поэтому они устанавливаются в одном месте (в таблице базы данных), независимо от того, в каких блоках они используются. Более того, платформа предоставляет экран Administration > Application Properties для управления свойствами, хранящимися в БД. Свойства, хранящиеся в файлах, должны быть установлены одновременно в соответствующих файлах блоков приложения.

Когда вам необходимо установить значение свойству приложения, определенному платформой, найдите это свойство в документации. Если в документации сказано, что свойство хранится в БД, для установки значения используйте экран Administration > Application Properties. В противном случае выясните в документации, какие блоки приложения используют свойство, и установите значение в файлах app.properties этих блоков. Например, если в документации сказано, что свойство используется во всех блоках, а ваше приложение состоит из Middleware и Web Client, установите свойство в файле app.properties модуля core и в файле web-app.properties модуля web. Параметры развертывания можно также установить вне проекта в файле local.app.properties. Подробнее см. Хранение свойств в файлах.

Свойства из компонентов приложения

Компонент приложения может предоставлять свойства путем объявления их в файле app-component.xml. Тогда если приложение, использующее компонент, не задает собственное значение свойства, значение будет получено из компонента. Если приложение использует несколько компонентов, предоставляющих одно и то же свойство, значение будет получено из компонента, который является ближайшим предком в иерархии зависимостей между компонентами. Если существует несколько компонентов на одном уровне иерархии, то значение свойства непредсказуемо.

Аддитивные свойства

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

Такие свойства должны быть сделаны аддитивными путем добавления знака плюс в начале значения. Этот знак говорит о том, что значение свойства во время выполнения должно быть собрано из компонентов приложения. Например, cuba.persistenceConfig - аддитивное свойство. В вашем проекте оно задает файл persistence.xml, определяющий модель данных проекта. Однако вследствие того, что реальное значение свойства будет также включать файлы persistence.xml компонентов приложения, полная модель данных вашего приложения будет включать также и сущности, определенные в компонентах.

Если знак + опустить, то значение будет получено только из текущего проекта. Это может быть полезным в случае, если наследование некоторой конфигурации из компонентов не требуется, например, при определении структуры главного меню.

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

Программный доступ к свойствам приложения

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

  • Через конфигурационный интерфейс. Если определить свойства с помощью аннотированных методов конфигурационного интерфейса, то код приложения будет иметь типизированный доступ к этим свойствам. Конфигурационные интерфейсы позволяют работать со свойствами всех типов хранения: в базе данных, в файлах и системных свойствах.

  • Методом getProperty() класса AppContext. Если вы установили свойство в файле или в системном свойстве Java, то код приложения может прочитать значение с помощью этого метода. Данный подход имеет следующие недостатки:

    • Не поддерживаются свойства, хранящиеся в базе данных.

    • В отличие от вызова метода интерфейса, вам необходимо передавать имя свойства в строке.

    • В отличие от получения результата нужного типа, вы можете получить только строковое значение свойства.

3.2.9.1. Хранение свойств в файлах

Свойства, определяющие конфигурацию и параметры развертывания, задаются в специальных файлах свойств, имеющих имя вида *app.properties. Каждый блок приложения имеет набор таких файлов, который задается в web.xml в параметре appPropertiesConfig.

Например, набор файлов свойств блока Middleware задается в файле web/WEB-INF/web.xml модуля core, и выглядит следующим образом:

<context-param>
    <param-name>appPropertiesConfig</param-name>
    <param-value>
        classpath:com/company/sample/app.properties
        /WEB-INF/local.app.properties
        "file:${app.home}/local.app.properties"
    </param-value>
</context-param>

Здесь префикс classpath: означает, что данный файл нужно искать в Java classpath, префикс file: − в файловой системе. Путь без такого префикса означает путь внутри веб-приложения относительно его корня. Возможно использование системных свойств Java, в данном случае app.home содержит путь к домашнему каталогу приложения.

Порядок перечисления файлов важен, так как значения, указанные в каждом последующем файле, заменяют значения одноименных свойств, заданные в предыдущих файлах. Если некоторого файла нет, он игнорируется.

Последний файл в приведенном наборе − local.app.properties. Он может использоваться для переопределения свойств приложения при развертывании.

Правила задания информации в файлах *.properties:

  • Кодировка файла - UTF-8

  • Ключ может состоять из латинских букв, цифр, точек и знаков подчеркивания

  • Значение пишется после знака равно (=)

  • Значение не нужно брать в кавычки " или '

  • Файловые пути записываются либо в UNIX-виде (/opt/haulmont/), либо в Windows-виде (c:\\haulmont\\)

  • Возможно использование кодов \n \t \r. Символ \ является зарезервированным, для вставки в значение экранируется сам собой (\\). Подробнее см.: http://docs.oracle.com/javase/tutorial/java/data/characters.html

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

3.2.9.2. Хранение свойств в базе данных

Свойства приложения, представляющие собой параметры времени выполнения, хранятся в таблице SYS_CONFIG базы данных.

Такие свойства имеют следующие особенности:

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

  • Значение может быть изменено и сохранено во время работы приложения следующими способами:

    • Через экран Administration > Application Properties.

    • Через JMX бин ConfigStorageMBean.

    • Если конфигурационный интерфейс, содержащий это свойство, имеет соответствующий setter, то свойство может изменено кодом приложения.

  • Значение свойства может быть переопределено для конкретного блока приложения в его файле app.properties, системным свойством Java или переменной окружения ОС.

Следует иметь в виду, что на клиентском уровне чтение свойства, хранящегося в БД, приводит к запросу к Middleware, что менее эффективно, чем чтение свойства из локального файла app.properties. Для уменьшения количества таких запросов клиент кэширует все свойства, хранящиеся в БД, на время жизни экземпляра реализации конфигурационного интерфейса. Поэтому если, например, в некотором экране UI необходимо несколько раз обратиться к свойствам одного конфигурационного интерфейса, лучше получить ссылку на него при инициализации экрана, и сохранить в поле для последующих обращений к одному и тому же экземпляру.

3.2.9.3. Конфигурационные интерфейсы

Данный механизм позволяет работать со свойствами приложения через методы Java-интерфейсов, что дает следующие преимущества:

  • Типизированность - прикладной код работает с нужными типами (String, Boolean, Integer и пр.), а не только со строками.

  • В прикладном коде вместо строковых идентификаторов свойств используются методы интерфейсов, имена которых проверяются компилятором и подсказываются средой разработки.

Пример получения значения таймаута транзакции в блоке Middleware:

@Inject
private ServerConfig serverConfig;

public void doSomething() {
    int timeout = serverConfig.getDefaultQueryTimeoutSec();
    ...
}

При невозможности инжекции можно получить ссылку на конфигурационный интерфейс через Configuration:

int timeout = AppBeans.get(Configuration.class)
        .getConfig(ServerConfig.class)
        .getDefaultQueryTimeoutSec();

Конфигурационные интерфейсы не являются нормальными бинами Spring, не пытайтесь получить их через AppBeans.get() - только непосредственной инжекцией самого интерфейса или через Configuration.getConfig().

3.2.9.3.1. Использование

Для создания конфигурационного интерфейса необходимо:

  • Создать интерфейс, унаследованный от com.haulmont.cuba.core.config.Config (не путать с классом сущности com.haulmont.cuba.core.entity.Config)

  • Добавить интерфейсу аннотацию @Source для указания источника (способа хранения) параметров:

    • SourceType.SYSTEM - значение свойства будет взято из системных свойств данной JVM, т.е. методом System.getProperty().

    • SourceType.APP - значение свойства будет взято из файлов app.properties.

    • SourceType.DATABASE - значение свойства будет взято из базы данных.

  • Создать методы доступа к свойству (getter / setter). Если значение свойства не предполагается изменять из кода приложения, метод доступа на запись не нужен. Тип, вовращаемый методом доступа на чтение, определяет тип свойства. Возможные типы рассмотрены ниже.

  • Добавить методу доступа на чтение аннотацию @Property, определяющую имя свойства.

  • Опционально аннотацию @Source можно задать для отдельного свойства в интерфейсе, если его источник отличается от заданного для всего интерфейса.

  • Если аннотация @Source имеет значение SourceType.DATABASE, то свойство можно редактировать на экране Administration > Application Properties. Если вы хотите, чтобы значение свойства было замаскировано, задайте на свойстве аннотацию @Secret. В этом случае на данном экране будет использован компонент PasswordField вместо обычного текстового поля.

Конфигурационный интерфейс должен быть определен внутри корневого пакета приложения (или во внутренних пакетах корневого пакета).

Например:

@Source(type = SourceType.DATABASE)
public interface SalesConfig extends Config {

    @Property("sales.companyName")
    String getCompanyName();

    @Property("sales.ftpPassword")
    @Secret
    String getFtpPassword();
}

Создавать класс реализации конфигурационного интерфейса не нужно - при получении ссылки на интерфейс инжекцией или через Configuration будет автоматически создан необходимый прокси-объект.

3.2.9.3.2. Типы свойств

"Из коробки" платформой поддерживаются следующие типы свойств:

  • String, простые типы и их объектные обертки (boolean, Boolean, int, Integer, etc.).

  • Перечисления (enum). Значение свойства сохраняется в файле или БД в виде имени значения перечисления.

    Если перечисление реализует интерфейс EnumClass и имеет статический метод fromId() для получения значения по идентификатору, с помощью аннотации @EnumStore можно задать хранение значения в виде идентификатора. Например:

    @Property("myapp.defaultCustomerGrade")
    @DefaultInteger(10)
    @EnumStore(EnumStoreMode.ID)
    CustomerGrade getDefaultCustomerGrade();
    
    @EnumStore(EnumStoreMode.ID)
    void setDefaultCustomerGrade(CustomerGrade grade);
  • Классы персистентных сущностей. При обращении к свойству типа сущности происходит загрузка из БД экземпляра, заданного значением свойства.

Для поддержки произвольного типа необходимо реализовать классы TypeStringify и TypeFactory для преобразования значения в строку и из нее, и указать эти классы для свойства с помощью аннотаций @Stringify и @Factory.

Рассмотрим этот процесс на примере типа UUID.

  1. Создаем класс com.haulmont.cuba.core.config.type.UuidTypeFactory унаследованный от com.haulmont.cuba.core.config.type.TypeFactory и реализуем в нем метод:

    public Object build(String string) {
        if (string == null) {
            return null;
        }
        return UUID.fromString(string);
    }
  2. TypeStringify создавать не нужно, т.к. по умолчанию будет использован метод toString() − в данном случае он нам подходит.

  3. Аннотируем свойство в конфигурационном интерфейсе:

    @Factory(factory = UuidTypeFactory.class)
    UUID getUuidProp();
    void setUuidProp(UUID value);

В платформе определены реализации TypeFactory и Stringify для следующих типов:

  • UUID - UuidTypeFactory, см. пример выше. TypeStringify здесь не требуется, так как реализация метода toString() по умолчанию подходит для типа UUID.

  • java.util.Date - DateFactory и DateStringify. Значение даты должно быть указано в формате yyyy-MM-dd HH:mm:ss.SSS, например:

    cuba.test.dateProp = 2013-12-12 00:00:00.000

    Пример описания свойства с типом Date в конфигурационном интерфейсе:

    @Property("cuba.test.dateProp")
    @Factory(factory = DateFactory.class)
    @Stringify(stringify = DateStringify.class)
    Date getDateProp();
    
    void setDateProp(Date date);
  • List<Integer> (список целых чисел) - IntegerListTypeFactory и IntegerListStringify. Значение свойства должно быть указано в виде списка чисел, разделенных пробелами, например:

    cuba.test.integerListProp = 1 2 3

    Пример описания свойства с типом List<Integer> в конфигурационном интерфейсе:

    @Property("cuba.test.integerListProp")
    @Factory(factory = IntegerListTypeFactory.class)
    @Stringify(stringify = IntegerListStringify.class)
    List<Integer> getIntegerListProp();
    
    void setIntegerListProp(List<Integer> list);
  • List<String> (список строк) - StringListTypeFactory и StringListStringify. Значение свойства должно быть указано в виде списка строк, разделенных символом "|", например:

    cuba.test.stringListProp = aaa|bbb|ccc

    Пример описания свойства с типом List<String> в конфигурационном интерфейсе:

    @Property("cuba.test.stringListProp")
    @Factory(factory = StringListTypeFactory.class)
    @Stringify(stringify = StringListStringify.class)
    List<String> getStringListProp();
    
    void setStringListProp(List<String> list);
3.2.9.3.3. Значения по умолчанию

Для свойств конфигурационных интерфейсов могут быть заданы значения по умолчанию. Эти значения будут возвращаться вместо null, если данный параметр не задан в месте хранения - в БД или в файле app.properties.

Значение по умолчанию может быть задано в виде строки с помощью аннотации @Default, либо в виде конкретного типа с помощью других аннотаций пакета com.haulmont.cuba.core.config.defaults:

@Property("cuba.email.adminAddress")
@Default("address@company.com")
String getAdminAddress();

@Property("cuba.email.delayCallCount")
@Default("2")
int getDelayCallCount();

@Property("cuba.email.defaultSendingAttemptsCount")
@DefaultInt(10)
int getDefaultSendingAttemptsCount();

@Property("cuba.test.dateProp")
@Default("2013-12-12 00:00:00.000")
@Factory(factory = DateFactory.class)
Date getDateProp();

@Property("cuba.test.integerList")
@Default("1 2 3")
@Factory(factory = IntegerListTypeFactory.class)
List<Integer> getIntegerList();

@Property("cuba.test.stringList")
@Default("aaa|bbb|ccc")
@Factory(factory = StringListTypeFactory.class)
List<String> getStringList();

Для сущностей значение по умолчанию задается строкой вида {entity_name}-{id}-{optional_view_name}, например:

@Default("sec$User-98e5e66c-3ac9-11e2-94c1-3860770d7eaf-browse")
User getAdminUser();

@Default("sec$Role-a294aef0-3ac9-11e2-9433-3860770d7eaf")
Role getAdminRole();

3.2.10. Локализация сообщений

Приложение на основе платформы CUBA поддерживает локализацию сообщений, то есть вывод всех элементов пользовательского интерфейса на языке, выбранном пользователем.

Руководство Localization in CUBA applications демонстрирует определение и использование локализованных сообщений в CUBA-приложениях.

Возможности выбора языка пользователем определяются комбинацией свойств приложения cuba.localeSelectVisible и cuba.availableLocales.

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

3.2.10.1. Пакеты сообщений

Пакет сообщений представляет собой набор файлов свойств с именами вида messages{_XX}.properties, расположенных в одном Java-пакете. Суффикс XX определяет язык, для которого в данном файле содержатся сообщения, и соответствует коду языка в Locale.getLanguage(). Возможно также использование остальных атрибутов Locale, например, country. В этом случая файл пакета будет иметь вид messages{XX_YY}.properties. Один из файлов пакета может быть без суффикса языка - это _файл по умолчанию. Именем пакета сообщений считается имя Java-пакета, в котором расположены файлы пакета.

Рассмотрим пример:

/com/abc/sales/gui/customer/messages.properties
/com/abc/sales/gui/customer/messages_fr.properties
/com/abc/sales/gui/customer/messages_ru.properties
/com/abc/sales/gui/customer/messages_en_US.properties

Данный пакет состоит из 4 файлов - один для русского языка, один для французского, один для американского английского (с кодом страны US) и один по умолчанию. Имя пакета - com.abc.sales.gui.customer

Файлы сообщений содержат пары ключ-значение, где ключ - это идентификатор сообщения, на который ссылается код приложения, а значение - само сообщение на языке данного файла. Правила задания пар аналогичны правилам файлов свойств java.util.Properties, со следующими особенностями:

  • Кодировка файла - обязательно UTF-8

  • Поддерживается включение других пакетов сообщений с помощью ключа @include, в том числе нескольких сразу - перечислением через запятую. При этом если некоторый ключ сообщения встречается и во включаемом пакете, и в текущем, будет использовано сообщение из текущего. Пример включения пакетов:

    @include=com.haulmont.cuba.web, com.abc.sales.web
    
    someMessage=Some Message
    ...

Получение сообщений из пакетов производится с помощью методов интерфейса Messages по следующим правилам:

  • Сначала производится поиск в конфигурационном каталоге приложения

    • Ищется файл messages_XX.properties в каталоге, задаваемом именем пакета сообщений, где XX - код требуемого языка

    • Если такого файла нет, в этом же каталоге ищется файл по умолчанию messages.properties

    • Если найден или файл нужного языка, или файл по умолчанию, он загружается вместе со всеми @include, и в нем ищется ключ сообщения

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

  • Если в конфигурационном каталоге сообщение не найдено, производится поиск в classpath по такому же алгоритму.

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

Рекомендуется организовывать пакеты сообщений следующим образом:

  • Если приложение не предполагает интернационализации, то можно не использовать пакеты и включать строки сообщений прямо в код приложения, либо пользоваться файлами по умолчанию messages.properties для отделения ресурсов от кода.

  • Если приложение интернациональное, логично файлы по умолчанию использовать для языка основной аудитории приложения, либо для английского языка. Именно сообщения из файлов по умолчанию будут показаны пользователю, если сообщений для нужного языка не найдено.

3.2.10.2. Главный пакет сообщений

Каждый стандартный блок приложения определяет для себя один главный пакет сообщений. Для блоков клиентского уровня этот пакет содержит названия пунктов главного меню и общих элементов UI (например, названия кнопок OK и Cancel). Для всех блоков приложения, включая Middleware, главный пакет определяет форматы преобразований Datatype.

Для указания главного пакета сообщений используется свойство приложения cuba.mainMessagePack. Значением свойства может быть либо один пакет, либо список пакетов, разделенный пробелами. Например:

cuba.mainMessagePack=com.haulmont.cuba.web com.abc.sales.web

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

Сообщения, заданные в пакетах базовых проектов CUBA, также можно переопределять в главном пакете сообщений проекта:

com.haulmont.cuba.gui.backgroundwork/backgroundWorkProgress.timeoutMessage = Новое сообщение об ошибке

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

IDX_SEC_USER_UNIQ_LOGIN = A user with the same login already exists
3.2.10.3. Локализация названий сущностей и атрибутов

Для отображения в UI локализованных названий сущностей и их атрибутов необходимо создать специальные пакеты сообщений в тех же Java-пакетах, что и сами сущности. Формат файлов сообщений должен быть следующим:

  • Ключ названия сущности - простое имя класса (без пакета)

  • Ключ названия атрибута - простое имя класса, затем через точку имя атрибута

Пример русской локализации сущности com.abc.sales.entity.Customer - файл /com/abc/sales/entity/messages_ru.properties:

Customer=Покупатель
Customer.name=Имя
Customer.email=Email

Order=Заказ
Order.customer=Покупатель
Order.date=Дата
Order.amount=Сумма

Такие пакеты сообщений, как правило, используются неявно для разработчика, например, визуальными компонентами Table и FieldGroup. Кроме того, названия сущностей и атрибутов могут быть также получены следующими методами:

  • программно - методами MessageTools getEntityCaption(), getPropertyCaption()

  • в XML-дескрипторе экрана - указанием ссылки на сообщение по правилам MessageTools.loadString: msg://{entity_package}/{key}, например,

    caption="msg://com.abc.sales.entity/Customer.name"
3.2.10.4. Локализация enum

Для локализации названий и значений перечислений необходимо в пакет сообщений, находящийся в Java-пакете класса перечисления добавить сообщения со следующими ключами:

  • Ключ названия перечисления - простое имя класса (без пакета)

  • Ключ значения - простое имя класса, затем через точку имя значения

Например, для перечисления

package com.abc.sales;

public enum CustomerGrade { PREMIUM, HIGH, STANDARD }

файл русской локализации /com/abc/sales/messages_ru.properties должен содержать строки:

CustomerGrade=Уровень покупателя
CustomerGrade.PREMIUM=Премиум
CustomerGrade.HIGH=Высокий
CustomerGrade.STANDARD=Стандартный

Локализованные значения перечислений автоматически используются различными визуальными компонентами, например, LookupField. Для программного получения локализованного значения перечисления можно использовать метод getMessage() интерфейса Messages, просто передавая в него экземпляр enum.

3.2.11. Аутентификация пользователей

В данном разделе рассмотрены некоторые аспекты управления доступом с точки зрения разработчика приложения. Для получения полной информации о возможностях и настройке ограничения доступа пользователей к данным см. Подсистема безопасности.

3.2.11.1. UserSession

Основной элемент подсистемы контроля доступа в CUBA-приложении - пользовательская сессия. Это объект класса UserSession, который ассоциирован с аутентифицированным в данный момент в системе пользователем, и содержит информацию о правах доступа пользователя к данным. Объект текущей сессии может быть получен в любом блоке приложения через интерфейс инфраструктуры UserSessionSource.

Пользовательская сессия создается на Middleware при выполнении метода AuthenticationManager.login() после аутентификации пользователя по переданному имени и паролю. Объект UserSession затем кэшируется в данном блоке Middleware, и возвращается на клиентский уровень. При работе в кластере объект сессии реплицируется на соседние узлы кластера Middleware. Клиентский блок, получив объект сессии, также сохраняет его у себя, так или иначе ассоциируя с активным пользователем (например, в HTTP сессии). Далее все вызовы Middleware для данного пользователя сопровождаются передачей идентификатора сессии (типа UUID), причем прикладному коду не нужно об этом заботиться - идентификатор сессии передается автоматически, независимо от сигнатуры вызываемых методов среднего слоя. Обработка вызовов клиентов на Middleware начинается с извлечения из кэша сессии по полученному идентификатору и установки ее в потоке выполнения. Объект сессии удаляется из кэша при вызове метода AuthenticationService.logout(), либо при истечении времени бездействия, определяемого свойством приложения cuba.userSessionExpirationTimeoutSec.

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

Объект UserSession содержит также методы для авторизации текущего пользователя, т.е. проверки его прав на объекты системы: isScreenPermitted(), isEntityOpPermitted(), isEntityAttrPermitted(), isSpecificPermitted().

С объектом UserSession могут быть ассоциированы именованные атрибуты произвольного сериализуемого типа. Атрибуты устанавливаются методом setAttribute() и возвращаются методом getAttribute(). Последний может также возвращать следующие параметры сессии, как если бы они были атрибутами:

  • userId - ID текущего зарегистрированного или замещенного пользователя;

  • userLogin - логин текущего зарегистрированного или замещенного пользователя в нижнем регистре.

Атрибуты реплицируются в кластере Middleware так же, как и все остальные данные сессии.

3.2.11.2. Вход в систему

Платформа предоставляет встроенные механизмы аутентификации, функциональность которых может быть расширена в приложениях. Они включают в себя различные схемы аутентификации, такие как вход по паролю, функциональность "Запомнить меня", доверенный и анонимный вход в систему.

Руководство Anonymous Access & Social Login содержит пример настройки публичного доступа к некоторым экранам приложения, а также реализации пользовательского входа в приложение с помощью учетной записи Google, Facebook или GitHub.

Данный раздел преимущественно описывает механизмы аутентификации среднего слоя. Для информации об аутентификации веб-клиента см. Процесс входа в Web Client.

Платформа включает следующие механизмы среднего слоя:

MiddlewareAuthenticationStructure
Рисунок 6. Механизмы аутентификации среднего слоя

Также платформа включает следующие дополнительные компоненты:

  • TrustedClientService, реализованный классом TrustedClientServiceBean - предоставляет анонимную/системную сессию для доверенных приложений-клиентов.

  • AnonymousSessionHolder - создаёт и хранит анонимную сессию для доверенных приложений-клиентов.

  • UserCredentialsChecker - проверяет, могут ли быть использованы переданные Credentials, например, для защиты от атак типа brute-force.

  • UserAccessChecker - проверяет, может ли пользователь выполнять вход из данного контекста, например, в REST API или с указанного IP адреса.

Основной интерфейс аутентификации - AuthenticationManager, включающий 4 метода:

public interface AuthenticationManager {

    AuthenticationDetails authenticate(Credentials credentials) throws LoginException;

    AuthenticationDetails login(Credentials credentials) throws LoginException;

    UserSession substituteUser(User substitutedUser);

    void logout();
}

Здесь есть два метода с похожей ответственностью: authenticate() и login(). Оба метода проверяют, являются ли переданные аутентификационные данные валидными и соответствуют ли они активному пользователю системы, затем возвращают объект AuthenticationDetails. Основное отличие в работе этих методов в том, что метод login() активирует сессию пользователя, так что она может использоваться в дальнейшем для вызова сервисов.

Объект Credentials представляет собой набор аутентификационных данных. Платформа поставляет несколько типов аутентификационных данных:

Доступные на всех слоях:

  • LoginPasswordCredentials

  • RememberMeCredentials

  • TrustedClientCredentials

Доступные только на среднем слое:

  • SystemUserCredentials

  • AnonymousUserCredentials

Методы login / authenticate AuthenticationManager возвращают объект AuthenticationDetails, который содержит объект UserSession. Этот объект может быть использован для проверки дополнительных разрешений, чтения свойств объекта User и атрибутов сессии. В платформе есть встроенная реализация интерфейса AuthenticationDetails - SimpleAuthenticationDetails, который хранит только объект сессии пользователя, приложения могут предоставлять свою реализацию AuthenticationDetails с дополнительной инфомацией для приложений-клиентов.

AuthenticationManager может выполнить метод authenticate() с одним из следующих результатов:

  • вернуть объект AuthenticationDetails, если он подтверждает, что переданные аутентификационные данные верны и соответствуют активному пользователю системы.

  • выбросить LoginException, если невозможно аутентифицировать пользователя, неверны аутентификационные данные или если пользователю запрещён доступ к системе.

  • выбросить UnsupportedCredentialsException, если переданный тип аутентификационных данных не поддерживается системой.

Реализация AuthenticationManager по умолчанию - AuthenticationManagerBean, который делегирует аутентификацию цепочке экземпляров AuthenticationProvider. AuthenticationProvider - это модуль аутентификации, который может обрабатывать объекты Credentials определённого типа. AuthenticationProvider также имеет специальный метод supports(), позволяющий узнать, поддерживается ли переданный тип аутентификационных данных.

LoginProcedure
Рисунок 7. Стандартный процесс входа пользователя

Стандартный процесс входа пользователя:

  • пользователь вводит свой логин и пароль

  • клиентский блок приложения вызывает метод Connection.login(), передавая ему логин пользователя и пароль.

  • Connection создаёт объект Credentials и вызывает метод login() сервиса AuthenticationService.

  • AuthenticationService делегирует выполнение бину AuthenticationManager, который использует цепочку объектов AuthenticationProvider. В этой цепочке имеется бин LoginPasswordAuthenticationProvider, поддерживающий аутентификационные данные типа LoginPasswordCredentials. Он загружает объект User по полученному логину, хэширует полученный хэш пароля повторно, используя в качестве соли идентификатор пользователя, и сравнивает полученный хэш с сохраненным в БД хэшем пароля. В случае несовпадения выбрасывается исключение LoginException.

  • После успешной аутентификации в созданный экземпляр UserSession загружаются все параметры доступа данного пользователя: список ролей, права, ограничения и атрибуты сессии.

  • Если журналирование пользовательских сессий активировано, в базу данных сохраняется запись с информацией о текущей сессии.

Алгоритм хэширования паролей реализуется бином типа EncryptionModule и задается в свойстве приложения cuba.passwordEncryptionModule. По умолчанию - BCrypt.

Встроенные провайдеры аутентификации

Платформа включает следующие реализации интерфейса AuthenticationProvider:

  • LoginPasswordAuthenticationProvider

  • RememberMeAuthenticationProvider

  • TrustedClientAuthenticationProvider

  • SystemAuthenticationProvider

  • AnonymousAuthenticationProvider

Все реализации загружают пользователя из базы данных, проверяют переданные аутентификационные данные и создают неактивный экземпляр пользовательской сессии при помощи UserSessionManager. Этот экземпляр может стать активным позднее, если вызывается метод AuthenticationManager.login().

Бины LoginPasswordAuthenticationProvider, RememberMeAuthenticationProvider и TrustedClientAuthenticationProvider используют дополнительные подключаемые проверки: бины, реализующие интерфейс UserAccessChecker. Если по крайней мере одна из таких проверок выбрасывает исключение LoginException, то аутентификация не выполняется и исключение LoginException передаётся вызывающему коду.

Кроме того, LoginPasswordAuthenticationProvider и RememberMeAuthenticationProvider проверяют аутентификационные данные при помощи бинов UserCredentialsChecker. Имеется одна встроенная реализация этого интерфейса - BruteForceUserCredentialsChecker, который проверяет, пытается ли пользователь подобрать верные аутентификационные данные при помощи атаки brute-force.

Исключения

AuthenticationManager и AuthenticationProvider могут выбрасывать исключение LoginException или одного из его наследников из методов authenticate() и login(). Исключение UnsupportedCredentialsException выбрасывается, если переданный объект аутентификационных данных Credentials не может быть обработан имеющимися AuthenticationProvider.

См. следующие классы исключений:

  • UnsupportedCredentialsException

  • LoginException

  • AccountLockedException

  • UserIpRestrictedException

  • RestApiAccessDeniedException

События

Стандартная реализация AuthenticationManager - AuthenticationManagerBean публикует следующие события во время аутентификации / входа пользователей:

  • BeforeAuthenticationEvent / AfterAuthenticationEvent

  • BeforeLoginEvent / AfterLoginEvent

  • AuthenticationSuccessEvent / AuthenticationFailureEvent

  • UserLoggedInEvent / UserLoggedOutEvent

  • UserSubstitutedEvent

Бины Spring на среднем слое приложения могут обрабатывать эти события при помощи механизма подписок @EventListener:

@Component
public class LoginEventListener {
    @Inject
    private Logger log;

    @EventListener
    protected void onUserLoggedIn(UserLoggedInEvent event) {
        User user = event.getUserSession().getUser();
        log.info("Logged in user {}", user.getLogin());
    }
}

Обработчики всех событий, перечисленных выше (кроме AfterLoginEvent, UserSubstitutedEvent и UserLoggedInEvent), могут выбросить LoginException, чтобы прервать процесс аутентификации / входа пользователя.

Например, мы можем реализовать механизм режима обслуживания системы, который позволит запретить вход в систему на время её обслуживания.

@Component
public class MaintenanceModeValve {
    private volatile boolean maintenance = true;

    public boolean isMaintenance() {
        return maintenance;
    }

    public void setMaintenance(boolean maintenance) {
        this.maintenance = maintenance;
    }

    @EventListener
    protected void onBeforeLogin(BeforeLoginEvent event) throws LoginException {
        if (maintenance && event.getCredentials() instanceof AbstractClientCredentials) {
            throw new LoginException("Sorry, system is unavailable");
        }
    }
}
Точки расширения

Вы можете расширить механизм аутентификации, используя следующие точки расширения:

  • AuthenticationService - заменить существующий AuthenticationServiceBean.

  • AuthenticationManager - заменить существующий AuthenticationManagerBean.

  • AuthenticationProvider - реализовать новый или заменить существующий бин AuthenticationProvider.

  • Events - реализовать обработчик одного из доступных событий.

Вы можете заменить существующие бины, задействуя механизмы Spring Framework, например, зарегистрировав новую реализацию в XML конфигурации Spring модуля core:

<bean id="cuba_LoginPasswordAuthenticationProvider"
      class="com.company.authext.core.CustomLoginPasswordAuthenticationProvider"/>
public class CustomLoginPasswordAuthenticationProvider extends LoginPasswordAuthenticationProvider {
    @Inject
    public CustomLoginPasswordAuthenticationProvider(Persistence persistence, Messages messages) {
        super(persistence, messages);
    }

    @Override
    public AuthenticationDetails authenticate(Credentials credentials) throws LoginException {
        LoginPasswordCredentials loginPassword = (LoginPasswordCredentials) credentials;
        // for instance, add new check before login
        if ("demo".equals(loginPassword.getLogin())) {
            throw new LoginException("Demo account is disabled");
        }

        return super.authenticate(credentials);
    }
}

Обработчики событий могут быть упорядочены при помощи аннотации @Order. Все бины и обработчики событий платформы используют значение order из диапазона 100 и 1000, что позволяет добавлять обработчики на уровне проект как до, так и после кода платформы. Если вы хотите добавить свой обработчик до обработчиков/бинов платформы - используйте значение меньше 100.

Задание order для обработчика события:

@Component
public class DemoEventListener {
    @Inject
    private Logger log;

    @Order(10)
    @EventListener
    protected void onUserLoggedIn(UserLoggedInEvent event) {
        log.info("Demo");
    }
}

Бины AuthenticationProvider могут реализовать интерфейс Ordered и метод getOrder() для определения очерёдности исполнения.

@Component
public class DemoAuthenticationProvider extends AbstractAuthenticationProvider
        implements AuthenticationProvider, Ordered {
    @Inject
    private UserSessionManager userSessionManager;

    @Inject
    public DemoAuthenticationProvider(Persistence persistence, Messages messages) {
        super(persistence, messages);
    }

    @Nullable
    @Override
    public AuthenticationDetails authenticate(Credentials credentials) throws LoginException {
        // ...
    }

    @Override
    public boolean supports(Class<?> credentialsClass) {
        return LoginPasswordCredentials.class.isAssignableFrom(credentialsClass);
    }

    @Override
    public int getOrder() {
        return 10;
    }
}
Дополнительные возможности
  • В платформе имеется механизм защиты от взлома пароля методом перебора. Для его включения необходимо установить свойство приложения cuba.bruteForceProtection.enabled для блока Middleware. В этом случае после определенного количества неуспешных попыток входа для определенного имени пользователя с определенного IP-адреса вход для пары логин + IP-адрес блокируется на некоторое время. Допустимое количество попыток входа для пары логин + IP-адрес определяется свойством приложения cuba.bruteForceProtection.maxLoginAttemptsNumber (по умолчанию 5). Интервал блокировки пользователя в секундах задается свойством cuba.bruteForceProtection.blockIntervalSec (по умолчанию 60).

  • Возможен вариант, когда пароль пользователя (точнее, хэш пароля) не хранится в базе данных, а проверяется внешними средствами, например, путем интеграции с LDAP. В этом случае фактически аутентификацию выполняет клиентский блок, а Middleware "доверяет" клиенту, создавая сессию по одному только логину пользователя без пароля методом AuthenticationService.login(), передавая TrustedClientCredentials. Этот метод требует выполнения следующих условий:

    • клиентский блок должен передать так называемый доверенный пароль, задаваемый на Middleware и на клиентском блоке свойством приложения cuba.trustedClientPassword

    • IP-адрес клиентского блока должен быть в списке, задаваемом свойством приложения cuba.trustedClientPermittedIpList

  • Вход в систему требуется также для автоматических процессов, запускаемых по расписанию, а также при подключении к бинам Middleware через JMX-интерфейс. Строго говоря, такие действия считаются административными и не требуют аутентификации до тех пор, пока не выполняется каких-либо изменений сущностей в базе данных. При записи сущностей в БД требуется проставить логин пользователя, который выполнил изменения, поэтому для работы таких процессов должен быть указан пользователь, от лица которого выполняются изменения.

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

    Вход в систему для процессов внутри Middleware выполняется вызовом AuthenticationManager.login() с передачей объекта SystemUserCredentials , содержащего логин пользователя (без пароля), от имени которого будет работать данный процесс. В результате создается объект UserSession, который будет закэширован в данном блоке Middleware и не будет реплицироваться в кластере.

Более подробно аутентификация процессов внутри Middleware рассмотрена в разделе Системная аутентификация.

3.2.11.3. SecurityContext

Экземпляр класса SecurityContext хранит информацию о пользовательской сессии для текущего потока выполнения. Он создается и передается в метод AppContext.setSecurityContext() в следующие моменты:

  • для блоков Web Client и Web Portal - в начале обработки каждого HTTP-запроса от пользовательского браузера.

  • для блока Middleware - в начале обработки каждого запроса от клиентского уровня и от назначенных заданий CUBA.

По окончании выполнения запроса в первых двух случаях SecurityContext удаляется из потока выполнения.

При создании прикладным кодом нового потока выполнения в него необходимо передать текущий экземпляр SecurityContext, например:

final SecurityContext securityContext = AppContext.getSecurityContext();
executor.submit(new Runnable() {
    public void run() {
        AppContext.setSecurityContext(securityContext);
        // business logic here
    }
});

То же самое можно сделать, используя обертки SecurityContextAwareRunnable или SecurityContextAwareCallable, например:

executor.submit(new SecurityContextAwareRunnable<>(() -> {
     // business logic here
}));
Future<String> future = executor.submit(new SecurityContextAwareCallable<>(() -> {
    // business logic here
    return some_string;
}));

3.2.12. Обработка исключений

В данном разделе рассмотрены различные аспекты генерации и обработки исключений в CUBA-приложениях.

3.2.12.1. Классы исключений

При создании собственных классов исключений следует придерживаться следующих правил:

  • Если исключение является нормальной частью бизнес-логики и при его возникновении требуется предпринимать некоторые нетривиальные действия, то класс исключения следует делать декларируемым (наследником Exception). Обработка таких исключений производится вызывающим кодом.

  • Если исключение сигнализирует об ошибочной ситуации, и реакцией на него должно быть прерывание хода выполнения и простое действие типа отображения информации об ошибке пользователю, то класс исключения следует делать недекларируемым (наследником RuntimeException). Обработка таких исключений производится специальными классами-обработчиками, зарегистрированными в клиентских блоках приложения.

  • Если исключение выбрасывается и обрабатывается в рамках одного блока приложения, то класс исключения следует объявлять в соответствующем модуле. Если же исключение выбрасывается на Middleware, а обрабатывается на клиентском уровне, то класс исключения необходимо объявлять в модуле global.

Платформа содержит специальный класс недекларируемого исключения SilentException, который можно использовать для прерывания хода выполнения без выдачи каких-либо сообщений пользователю или в лог. SilentException объявлен в модуле global, поэтому доступен как на Middleware, так и в клиентских блоках.

3.2.12.2. Передача исключений Middleware

Если при выполнении запроса от клиента на Middleware возникает исключение, выполнение прерывается и на клиента возвращается объект исключения, как правило, включающий цепочку порождающих друг друга исключений. Так как цепочка исключений может содержать классы, недоступные клиентскому блоку (например, исключения JDBC-драйвера), на клиента передается не сама эта цепочка, а ее представление внутри специального создаваемого исключения RemoteException.

Информация об исключениях-причинах сохраняется в виде списка объектов RemoteException.Cause. Каждый объект Cause хранит обязательно имя класса исключения и его сообщение. Кроме того, если класс исключения "поддерживается клиентом", то Cause содержит также и сам объект исключения. Это дает возможность передать на клиента информацию в полях исключения.

Класс исключения, объекты которого нужно передавать на клиентский уровень именно в виде Java-объектов, нужно аннотировать @SupportedByClient, например:

@SupportedByClient
public class WorkflowException extends RuntimeException {
...

Таким образом, при возникновении на Middleware исключения, не аннотированного @SupportedByClient, вызывающий клиентский код получит RemoteException, внутри которого будет находиться исходное исключение в виде строки. Если же исходное исключение аннотировано @SupportedByClient, то вызывающий код получит именно его. Это дает возможность в прикладном коде организовывать обработку декларируемых сервисами Middleware исключений традиционным образом - с помощью блоков try/catch.

Следует иметь в виду, что чтобы поддерживаемое клиентом исключение было действительно передано на клиента в виде объекта, оно не должно содержать внутри себя в цепочке getCause() неподдерживаемых исключений. Поэтому если вы создаете на Middleware экземпляр исключения и хотите передать его на клиента, указывайте для него параметр cause только если вы уверены, что он содержит только исключения, известные клиенту.

Упаковку объектов исключений в RemoteException перед передачей на клиентский уровень выполняет перехватчик вызовов сервисов - класс ServiceInterceptor. Кроме того, он же выполняет логирование исключений. По умолчанию в журнал выводится вся информация об исключении, включая полный stack trace. Если это нежелательно, можно добавить классу исключения аннотацию @Logging, указав в ней тип логирования:

  • FULL - (по умолчанию) полная информация, включая stacktrace

  • BRIEF - только имя класса исключения и сообщение

  • NONE - не выводить ничего

Например:

@SupportedByClient
@Logging(Logging.Type.BRIEF)
public class FinancialTransactionException extends Exception {
...
3.2.12.3. Обработчики исключений клиентского уровня

Необработанные исключения, возникшие на клиентском уровне или переданные с Middleware, попадают в специальный механизм обработчиков блока Web Client.

Обработчик должен быть Spring-бином, реализовывать интерфейс UiExceptionHandler, в методе handle() которого производить обработку и возвращать true, либо сразу возвращать false, если данный обработчик не может обработать переданное ему исключение. Такое поведение позволяет организовать "цепочку ответственности" обработчиков.

Рекомендуется наследовать классы своих обработчиков от базового класса AbstractUiExceptionHandler, который умеет разбирать цепочку исключений (с учетом упакованных внутри RemoteException) и реагировать на конкретные типы исключений. Типы исключений, для которых предназначен данный обработчик, указываются в массиве строк, передаваемом в конструкторе обработчика базовому конструктору. Каждая строка массива должна содержать одно полное имя класса обрабатываемого исключения.

Предположим, имеется следующий класс исключения:

package com.company.demo.web;

public class ZeroBalanceException extends RuntimeException {

    public ZeroBalanceException() {
        super("Insufficient funds in your account");
    }
}

Тогда обработчик должен иметь следующий конструктор:

@Component("demo_ZeroBalanceExceptionHandler")
public class ZeroBalanceExceptionHandler extends AbstractUiExceptionHandler {

    public ZeroBalanceExceptionHandler() {
        super(ZeroBalanceException.class.getName());
    }
...

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

@Component("sample_ForeignKeyViolationExceptionHandler")
public class ForeignKeyViolationExceptionHandler extends AbstractUiExceptionHandler {

    public ForeignKeyViolationExceptionHandler() {
        super("java.sql.SQLIntegrityConstraintViolationException");
    }
...

В случае использования в качестве базового класса AbstractUiExceptionHandler логика обработки располагается в методе doHandle(), и может выглядеть следующим образом:

package com.company.demo.web;

import com.haulmont.cuba.gui.Notifications;
import com.haulmont.cuba.gui.exception.AbstractUiExceptionHandler;
import org.springframework.stereotype.Component;
import javax.annotation.Nullable;

@Component("demo_ZeroBalanceExceptionHandler")
public class ZeroBalanceExceptionHandler extends AbstractUiExceptionHandler {

    public ZeroBalanceExceptionHandler() {
        super(ZeroBalanceException.class.getName());
    }

    @Override
    protected void doHandle(String className, String message, @Nullable Throwable throwable, UiContext context) {
        context.getNotifications().create(Notifications.NotificationType.ERROR)
                .withCaption("Error")
                .withDescription(message)
                .show();
    }
}

Если имени класса исключения недостаточно для того, чтобы принять решение о применимости данного обработчика к исключению, следует определить метод canHandle(), получающий кроме прочего текст исключения. Метод должен вернуть true, если данный обработчик применим для исключения. Например:

package com.company.demo.web.exceptions;

import com.haulmont.cuba.gui.Notifications;
import com.haulmont.cuba.gui.exception.AbstractUiExceptionHandler;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.annotation.Nullable;

@Component("demo_ZeroBalanceExceptionHandler")
public class ZeroBalanceExceptionHandler extends AbstractUiExceptionHandler {

    public ZeroBalanceExceptionHandler() {
        super(ZeroBalanceException.class.getName());
    }

    @Override
    protected void doHandle(String className, String message, @Nullable Throwable throwable, UiContext context) {
        context.getNotifications().create(Notifications.NotificationType.ERROR)
                .withCaption("Error")
                .withDescription(message)
                .show();
    }

    @Override
    protected boolean canHandle(String className, String message, @Nullable Throwable throwable) {
        return StringUtils.containsIgnoreCase(message, "Insufficient funds in your account");
    }
}

Интерфейс Dialogs, доступный через параметр UiContext метода doHandle(), предоставляет специальный диалог для отображения исключений, содержащий схлопываемое поле с полным stack trace исключения. Данный диалог используется в обработчике по умолчанию, но вы можете использовать его и для конкретных исключений:

@Override
protected void doHandle(String className, String message, @Nullable Throwable throwable, UiContext context) {
    if (throwable != null) {
        context.getDialogs().createExceptionDialog()
                .withThrowable(throwable)
                .withCaption("Error")
                .withMessage(message)
                .show();
    } else {
        context.getNotifications().create(Notifications.NotificationType.ERROR)
                .withCaption("Error")
                .withDescription(message)
                .show();
    }
}
3.2.12.3.1. Обработка исключений нарушения уникальности

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

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

IDX_SEC_USER_UNIQ_LOGIN = Пользователь с таким логином уже существует

Так, например, если вы получили уведомление:

unique constraint message

и затем добавили

IDX_DEMO_PRODUCT_UNIQ_NAME = A product with this name already exists

к главному пакету сообщений, вы получите следующее уведомление:

unique constraint message 2

Распознавание ошибок нарушения уникальности производится классом UniqueConstraintViolationHandler, который использует регулярные выражения, зависящие от типа используемой базы данных. Если стандартное выражение не распознает ошибки уникальности вашей БД, задайте подходящее выражение с помощью свойства приложения cuba.uniqueConstraintViolationPattern.

Вы также можете полностью заменить стандартный обработчик, предоставив свой собственный обработчик исключений с более высоким приоритетом, например @Order(HIGHEST_PLATFORM_PRECEDENCE - 10).

3.2.13. Bean Validation

Bean Validation - опциональный механизм, обеспечивающий единообразную валидацию данных на среднем слое, в Generic UI и в REST API. Он основан на спецификации JSR 380 - Bean Validation 2.0 и ее референсной имплементации: Hibernate Validator.

3.2.13.1. Задание ограничений

Ограничения bean validation задаются с помощью аннотаций пакета javax.validation.constraints или собственных аннотаций. Аннотации указываются на декларации класса сущности или POJO, на поле или getter-методе, а также на методе сервиса middleware.

Пример использования стандартных аннотаций валидации на полях сущности:

@Table(name = "DEMO_CUSTOMER")
@Entity(name = "demo_Customer")
public class Customer extends StandardEntity {

    @Size(min = 3) // length of value must be longer then 3 characters
    @Column(name = "NAME", nullable = false)
    protected String name;

    @Min(1) // minimum value
    @Max(5) // maximum value
    @Column(name = "GRADE", nullable = false)
    protected Integer grade;

    @Pattern(regexp = "\\S+@\\S+") // value must conform to the pattern
    @Column(name = "EMAIL")
    protected String email;

    //...
}

Пример использования собственной аннотации уровня класса (см. ниже):

@CheckTaskFeasibility(groups = {Default.class, UiCrossFieldChecks.class}) // custom validation annotation
@Table(name = "DEMO_TASK")
@Entity(name = "demo_Task")
public class Task extends StandardEntity {
    //...
}

Пример валидации параметров и возвращаемого значения метода сервиса:

public interface TaskService {
    String NAME = "demo_TaskService";

    @Validated // indicates that the method should be validated
    @NotNull
    String completeTask(@Size(min = 5) String comment, @Valid @NotNull Task task);
}

Аннотация @Valid может быть использована для каскадной валидации параметров метода. В примере выше все ограничения, заданные для объекта Task, также будут валидированы.

Группы ограничений

Группы ограничений позволяют применять подмножество всех заданных ограничений в зависимости от логики приложения. Например, вы можете заставить пользователя ввести значение некоторого атрибута сущности в UI, и в то же время иметь возможность установить данный атрибут в null в некотором внутреннем механизме. Для этого необходимо указать атрибут groups в аннотации ограничения, и оно будет действовать только когда эта же группа передается в механизм валидации.

Платформа передает в механизм валидации следующие группы ограничений:

  • RestApiChecks - при валидации в REST API.

  • ServiceParametersChecks - при валидации параметров сервисов.

  • ServiceResultChecks - при валидации возвращаемых значений сервисов.

  • UiComponentChecks - при валидации отдельных полей в UI.

  • UiCrossFieldChecks - при валидации ограничений уровня класса на коммите экрана редактора сущности.

  • javax.validation.groups.Default - данная группа передается во всех случаях кроме коммита экрана редактора сущности.

Сообщения валидации

Ограничения могут иметь сообщения для отображения пользователям.

Сообщения могут быть указаны непосредственно в аннотациях валидации, например:

@Pattern(regexp = "\\S+@\\S+", message = "Invalid format")
@Column(name = "EMAIL")
protected String email;

Сообщения можно также поместить в пакет локализованных сообщений и использовать следующий формат указания сообщения в аннотации: {msg://message_pack/message_key} или, в сокращённом виде, {msg://message_key} (только для сущностей). Например:

@Pattern(regexp = "\\S+@\\S+", message = "{msg://com.company.demo.entity/Customer.email.validationMsg}")
@Column(name = "EMAIL")
protected String email;

или, если ограничение задано для сущности и сообщение находится в пакете сообщений данной сущности:

@Pattern(regexp = "\\S+@\\S+", message = "{msg://Customer.email.validationMsg}")
@Column(name = "EMAIL")
protected String email;

Сообщения могут содержать параметры и выражения. Параметры заключаются в фигурные скобки {} и представляют собой либо указатели на локализованные сообщения (см. выше) или параметры аннотации, например, {min}, {max}, {value}. Выражения заключаются в фигурные скобки со знаком доллара ${} и могут включать валидируемое значение в виде переменной validatedValue, параметры аннотации типа value или min, и выражения JSR-341 (EL 3.0). Например:

@Pattern(regexp = "\\S+@\\S+", message = "Invalid email: ${validatedValue}, pattern: {regexp}")
@Column(name = "EMAIL")
protected String email;

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

Собственные ограничения

В проекте можно создать собственные ограничения с программной или декларативной валидацией.

Для создания ограничения с программной валидацией выполните следующее:

  1. Создайте аннотацию в модуле global проекта и добавьте ей аннотацию @Constraint. Ваша аннотация должна содержать атрибуты message, groups и payload:

    @Target({ ElementType.TYPE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = TaskFeasibilityValidator.class)
    public @interface CheckTaskFeasibility {
    
        String message() default "{msg://com.company.demo.entity/CheckTaskFeasibility.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    }
  2. Создайте класс валидатора в модуле global проекта:

    public class TaskFeasibilityValidator implements ConstraintValidator<CheckTaskFeasibility, Task> {
    
        @Override
        public void initialize(CheckTaskFeasibility constraintAnnotation) {
        }
    
        @Override
        public boolean isValid(Task value, ConstraintValidatorContext context) {
            Date now = AppBeans.get(TimeSource.class).currentTimestamp();
            return !(value.getDueDate().before(DateUtils.addDays(now, 3)) && value.getProgress() < 90);
        }
    }
  3. Используйте аннотацию:

    @CheckTaskFeasibility(groups = UiCrossFieldChecks.class)
    @Table(name = "DEMO_TASK")
    @Entity(name = "demo_Task")
    public class Task extends StandardEntity {
    
        @Future
        @Temporal(TemporalType.DATE)
        @Column(name = "DUE_DATE")
        protected Date dueDate;
    
        @Min(0)
        @Max(100)
        @Column(name = "PROGRESS", nullable = false)
        protected Integer progress;
    
        //...
    }

Собственные аннотации могут также быть созданы как композиции имеющихся, например:

@NotNull
@Size(min = 2, max = 14)
@Pattern(regexp = "\\d+")
@Target({METHOD, FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = {})
public @interface ValidProductCode {
    String message() default "{msg://om.company.demo.entity/ValidProductCode.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

При использовании композитных ограничений результирующий набор нарушений ConstraintViolation будет содержать отдельные записи для каждого включенного ограничения. Для того, чтобы получить одну запись нарушения, добавьте @ReportAsSingleViolation классу вашей аннотации.

Аннотации валидации, заданные в CUBA

Кроме стандартных аннотаций из пакета javax.validation.constraints можно использовать следующую аннотацию, определенную в платформе:

  • @RequiredView - может быть добавлена на методы сервисов для того, чтобы во время выполнения убедиться, что в сущностях загружены все атрибуты, заданные в требуемых представлениях. Если аннотация задана на методе, то проверяется результат метода. Если аннотация задана на параметре, проверяется этот параметр. Если параметр или результат являются коллекцией, то проверяются все элементы этой коллекции. Пример использования:

public interface MyService {
    String NAME = "sample_MyService";

    @Validated
    void processFoo(@RequiredView("foo-view") Foo foo);

    @Validated
    void processFooList(@RequiredView("foo-view") List<Foo> fooList);

    @Validated
    @RequiredView("bar-view")
    Bar loadBar(@RequiredView("foo-view") Foo foo);
}
3.2.13.2. Запуск валидации
Валидация в UI

Компоненты Generic UI, соединенные с источником данных, получают экземпляр BeanValidator для проверки значения. Валидатор вызывается из метода Component.Validatable.validate(), реализуемого компонентом, и может выбрасывать исключение CompositeValidationException, содержащее набор объектов нарушений.

Стандартный валидатор может быть программно удален или проинициализирован другой группой ограничений:

@UiController("sample_NewScreen")
@UiDescriptor("new-screen.xml")
public class NewScreen extends Screen {

    @Inject
    private TextField<String> field1;
    @Inject
    private TextField<String> field2;

    @Subscribe
    protected void onInit(InitEvent event) {
        field1.getValidators().stream()
                .filter(BeanPropertyValidator.class::isInstance)
                .forEach(field1::removeValidator); (1)

        field2.getValidators().stream()
                .filter(BeanPropertyValidator.class::isInstance)
                .forEach(validator -> {
                    ((BeanPropertyValidator) validator).setValidationGroups(new Class[] {UiComponentChecks.class}); (2)
                });
    }
}
1 Полностью удаляем валидацию с компонента.
2 Здесь валидаторы будут проверять только ограничения, явно установленные группой UiComponentChecks, так как группа по умолчанию не передаётся.

По умолчанию, BeanValidator содержит две группы: Default и UiComponentChecks.

Если атрибут сущности аннотирован @NotNull без группы ограничений, он будет помечен как обязательный в метаданных, и UI-компоненты работающие с данным атрибутом через источник данных, будут иметь свойство required = true.

Компоненты DateField и DatePicker автоматически устанавливают свои свойства rangeStart и rangeEnd в соответствии с аннотациями @Past, @PastOrPresent, @Future, @FutureOrPresent.

Экраны редактирования выполняют валидацию ограничений уровня класса при коммите, если ограничения включают группу UiCrossFieldChecks и все проверки ограничений уровня атрибутов прошли успешно. Валидацию данного типа можно отключить с помощью метода контроллера setCrossFieldValidate():

public class EventEdit extends StandardEditor<Event> {
    @Subscribe
    public void onInit(InitEvent event) {
        setCrossFieldValidate(false);
    }
}
Валидация в DataManager

DataManager может выполнять валидацию сохраняемых сущностей. Следующие параметры оказывают влияние на валидацию:

  • Свойство приложения cuba.dataManagerBeanValidation устанавливает глобальный признак, выполнять ли валидацию по умолчанию.

  • Глобальный признак можно переопределить, устанавливая значение типа CommitContext.ValidationMode в CommitContext при выполнении DataManager.commit(), или в DataContext.PreCommitEvent при сохранении данных в экране UI.

  • В CommitContext и в DataContext.PreCommitEvent можно передать список групп валидации для применения некоторого подмножества определенных ограничений.

Валидация сервисов Middleware

Сервисы среднего слоя выполняют валидацию параметров и результатов методов, если метод имеет аннотацию @Validated в интерфейсе сервиса. Например:

public interface TaskService {
    String NAME = "demo_TaskService";

    @Validated
    @NotNull
    String completeTask(@Size(min = 5) String comment, @NotNull Task task);
}

Аннотация @Validated может содержать указание групп ограничений для применения подмножества имеющихся ограничений. Если группы не указаны, то по умолчанию используются следующие:

  • Default и ServiceParametersChecks - для параметров методов

  • Default и ServiceResultChecks - для возвращаемых результатов методов

При возникновении ошибок валидации выбрасываются исключения MethodParametersValidationException и MethodResultValidationException.

При выполнении некоторой специфической программной валидации в сервисе, можно использовать исключение CustomValidationException для того, чтобы клиенты получали информацию об ошибках в том же формате что и от стандартной валидации. Это может быть особенно актуально для клиентов REST API.

Валидация в REST API

Универсальный REST API автоматически выполняет bean validation для действий создания и изменения сущностей. Ошибки валидации передаются клиенту следующим образом:

  • MethodResultValidationException и ValidationException порождают HTTP статус 500 Server error

  • MethodParametersValidationException, ConstraintViolationException и CustomValidationException порождают HTTP статус 400 Bad request

  • Тело ответа с Content-Type: application/json будет содержать список объектов со свойствами message, messageTemplate, path и invalidValue, например:

    [
        {
            "message": "Invalid email: aaa",
            "messageTemplate": "{msg://com.company.demo.entity/Customer.email.validationMsg}",
            "path": "email",
            "invalidValue": "aaa"
        }
    ]
    • path содержит путь к невалидному атрибуту в валидируемом графе объектов

    • messageTemplate содержит сторку, заданную в атрибуте message аннотации

    • message содержит сообщение ошибки валидации

    • invalidValue возвращается только если тип значения один из следующих: String, Date, Number, Enum, UUID.

Программная валидация

Bean validation можно запустить программно используя интерфейс инфраструктуры BeanValidation, доступный и на middleware и на клиентском уровне. Через данный интерфейс необходимо получить реализацию javax.validation.Validator, которая и запускает валидацию. Результатом валидации является набор объектов типа ConstraintViolation. Например:

@Inject
private BeanValidation beanValidation;

public void save(Foo foo) {
    Validator validator = beanValidation.getValidator();
    Set<ConstraintViolation<Foo>> violations = validator.validate(foo);
    // ...
}

См. статью Валидация в Java-приложениях в нашем блоге для получения более подробной информации. Пример приложения, которое использовалось в этой статье, доступен на GitHub.

3.2.14. Контроль доступа к атрибутам сущностей

Подсистема безопасности позволяет управлять доступом к атрибутам сущностей в соответствии с правами пользователя. Т.е. фреймворк может сделать атрибут read-only или скрыть его в зависимости от набора ролей, назначенных текущему пользователю. Однако, иногда может потребоваться изменять доступ к атрибуту в зависимости также от текущего состояния экземпляра сущности или связанных сущностей.

Механизм контроля доступа к атрибутам позволяет создавать правила того, какие атрибуты должны быть скрыты, нередактируемы или обязательны к заполнению для некоторого экземпляра сущности, и применять эти правила к компонентам Generic UI и в REST API.

Данный механизм работает следующим образом:

  • Когда DataManager загружает сущность, он находит бины, реализующие интерфейс SetupAttributeAccessHandler и вызывает их метод setupAccess(), передавая в них объект типа SetupAttributeAccessEvent. Данный объект содержит загруженный экземпляр в состоянии managed и три коллекции имен атрибутов: read-only, hidden и required (изначально они пустые).

  • Имплементации SetupAttributeAccessHandler анализируют состояние сущности и заполняют списки имен атрибутов соответствующим образом. Данные классы являются по сути контейнерами правил, задающих доступ к атрибутам экземпляров.

  • Описываемый механизм сохраняет имена атрибутов, заданные правилами, в самом экземпляре (в связанном объекте SecurityState).

  • На клиентском уровне, Generic UI и REST API используют объект SecurityState для управления доступом к атрибутам сущностей.

Для создания правил для некоторого типа сущностей, необходимо выполнить следующее:

  • Создайте Spring-бин в модуле core проекта и реализуйте в нем интерфейс SetupAttributeAccessHandler. Параметризуйте интерфейс типом обрабатываемой сущности. Бин должен иметь дефолтный singleton scope. Вам необходимо реализовать методы интерфейса:

    • supports(Class) - возвращает true если данный обработчик предназначен для работы с сущностями переданного класса.

    • setupAccess(SetupAttributeAccessEvent) - оперирует коллекциями атрибутов для настройки доступа. В данном методе необходимо заполнить коллекции скрываемых, только для чтения и обязательных атрибутов используя методы addHidden(), addReadOnly() и addRequired() объекта события. Экземпляр сущности, доступный через метод getEntity(), находится в состоянии managed, поэтому можно безопасно обращаться ко всем его атрибутам и атрибутам связанных сущностей.

Рассмотрим пример сущности Order, имеющей атрибуты customer и amount. Для ограничения доступа к атрибуту amount в зависимости от customer можно создать следующее правило:

package com.company.sample.core;

import com.company.sample.entity.Order;
import com.haulmont.cuba.core.app.SetupAttributeAccessHandler;
import com.haulmont.cuba.core.app.events.SetupAttributeAccessEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component("sample_OrderAttributeAccessHandler")
public class OrderAttributeAccessHandler implements SetupAttributeAccessHandler<Order> {

    @Override
    public boolean supports(Class clazz) {
        return Order.class.isAssignableFrom(clazz);
    }

    @Override
    public void setupAccess(SetupAttributeAccessEvent<Order> event) {
        Order order = event.getEntity();
        if (order.getCustomer() != null) {
            if ("PLATINUM".equals(order.getCustomer().getGrade().getCode())) {
                event.addHidden("amount");
            } else if ("GOLD".equals(order.getCustomer().getGrade().getCode())) {
                event.addReadOnly("amount");
            }
        }
    }
}
Контроль доступа к атрибутам в Generic UI

Фреймворк автоматически применяет ограничения доступа к экрану в момент между посылкой BeforeShowEvent и AfterShowEvent. Если вы не хотите применять ограничения для некоторого экрана, добавьте контроллеру экрана аннотацию @DisableAttributeAccessControl.

Пересчитать и применить ограничения к экрану можно и когда он уже открыт, в ответ на действия пользователя. Для этого необходимо использовать бин AttributeAccessSupport, передавая ему текущий экран и сущность, состояние которой изменилось. Например:

@UiController("sales_Order.edit")
@UiDescriptor("order-edit.xml")
@EditedEntityContainer("orderDc")
@LoadDataBeforeShow
public class OrderEdit extends StandardEditor<Order> {

    @Inject
    private AttributeAccessSupport attributeAccessSupport;

    @Subscribe(id = "orderDc", target = Target.DATA_CONTAINER)
    protected void onOrderDcItemPropertyChange(InstanceContainer.ItemPropertyChangeEvent<Order> event) {
        if ("customer".equals(event.getProperty())) {
            attributeAccessSupport.applyAttributeAccess(this, true, getEditedEntity());
        }
    }

}

Второй параметр метода applyAttributeAccess() - булевское значение, которое указывает, нужно ли сбрасывать ограничения доступа к компонентам в дефолтные настройки перед тем, как применить новые. Если передано true, возможные программные изменения в этих настройках будут потеряны. Когда данный метод вызывается автоматически перед открытием окна, передается false. Когда же вы вызываете данный метод в ответ на UI-события, передавайте true, иначе ограничения компонентов будут суммироваться, а не заменяться.

Ограничения доступа к атрибутам применяются только к компонентам, связанным с одним атрибутом сущности, например TextField или LookupField. Table и другие компоненты, реализующие интерфейс ListComponent, не затрагиваются. Поэтому если вы пишете правило, скрывающее атрибут для некоторых экземпляров, рекомендуется не показывать этот атрибут в таблицах совсем.

3.3. Базы данных

В данном разделе приведена информация о возможных типах СУБД приложений на платформе CUBA. Кроме того, описан механизм миграции БД, с помощью которого можно создать новую базу данных, и в дальнейшем поддерживать ее в актуальном состоянии на протяжении всего цикла разработки и эксплуатации приложения.

Компоненты работы с базой данных принадлежат блоку Middleware, другие блоки приложения не имеют прямого доступа к БД.

3.3.1. Подключение к базам данных

CUBA-приложение получает соединение с базой данных через JDBC DataSource. Источник данных может быть сконфигурирован в приложении или получен из JNDI. Способ получения источника данных задается свойством приложения cuba.dataSourceProvider: его значение может быть либо application, либо jndi.

Соединения с главным и дополнительным хранилищами можно легко сконфигурировать в CUBA Studio, см. его документацию. Информация, приведенная ниже, может быть полезна при поиске проблем и при необходимости задания параметров, недоступных в Studio, например параметров пула соединений.

Конфигурирование источника данных в приложении

Если источник данных сконфигурирован в приложении, фреймворк создает пул соединений используя HikariCP. При этом и параметры соединения, и параметры пула задаются свойствами приложения, расположенными в файле app.properties модуля core. Данный способ является рекомендуемым, если вам не нужен специфический пул соединений, предоставляемый сервером приложения.

Следующие свойства приложения задают тип БД и параметры соединения:

  • cuba.dbmsType - задает тип СУБД.

  • cuba.dataSourceProvider - значение application указывает, что источник данных должен быть сконфигурирован свойствами приложения.

  • cuba.dataSource.username - имя пользователя БД.

  • cuba.dataSource.password - пароль пользователя БД.

  • cuba.dataSource.dbName - имя БД.

  • cuba.dataSource.host - имя хоста сервера БД.

  • cuba.dataSource.port - необязательный параметр, задает порт сервера БД, если он отличается от стандартного для данного типа СУБД.

  • cuba.dataSource.jdbcUrl - необязательный параметр, задает полный JDBC URL если необходимо передать дополнительные параметры соединения. При этом все остальные отдельные параметры описанные выше все равно необходимы для работы задач миграции.

Для конфигурирования параметров пула соединений необходимо указать свойства HikariCP с префиксом cuba.dataSource., например cuba.dataSource.maximumPoolSize или cuba.dataSource.connectionTimeout. См. полный список поддерживаемых свойств и их значений по умолчанию в документации по HikariCP.

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

Например:

# main data store connection parameters
cuba.dbmsType = hsql
cuba.dataSourceProvider = application
cuba.dataSource.username = sa
cuba.dataSource.password =
cuba.dataSource.dbName = demo
cuba.dataSource.host = localhost
cuba.dataSource.port = 9111
cuba.dataSource.maximumPoolSize = 20

# names of additional data stores
cuba.additionalStores = clients,orders

# 'clients' data store connection parameters
cuba.dbmsType_clients = postgres
cuba.dataSourceProvider_clients = application
cuba.dataSource_clients.username = postgres
cuba.dataSource_clients.password = postgres
cuba.dataSource_clients.dbName = clients_db
cuba.dataSource_clients.host = localhost

# 'orders' data store connection parameters
cuba.dbmsType_orders = mssql
cuba.dataSourceProvider_orders = application
cuba.dataSource_orders.jdbcUrl = jdbc:sqlserver://localhost;databaseName=orders_db;currentSchema=my_schema
cuba.dataSource_orders.username = sa
cuba.dataSource_orders.password = myPass123
cuba.dataSource_orders.dbName = orders_db
cuba.dataSource_orders.host = localhost

Кроме того, для каждого хранилища необходимо в файле spring.xml модуля core задать определение бина CubaDataSourceFactoryBean с соответствующим параметром storeName. Например:

<bean id="cubaDataSource_clients" class="com.haulmont.cuba.core.sys.CubaDataSourceFactoryBean">
    <property name="storeName" value="clients"/>
</bean>

<bean id="cubaDataSource_orders" class="com.haulmont.cuba.core.sys.CubaDataSourceFactoryBean">
    <property name="storeName" value="orders"/>
</bean>

Если источник данных сконфигурирован в приложении, задачи Gradle по миграции БД могут не иметь параметров, так как они могут быть получены из свойств приложения. Это является дополнительным преимуществом данного способа определения источника даннных. Например:

task createDb(dependsOn: assembleDbScripts, description: 'Creates local database', type: CubaDbCreation) {
}

task updateDb(dependsOn: assembleDbScripts, description: 'Updates local database', type: CubaDbUpdate) {
}
Получение источника данных из JNDI

Если необходимо использовать источник данных, предоставляемый сервером приложения через JNDI, задайте следующие свойства приложения в файле app.properties модуля core:

  • cuba.dbmsType - задает тип СУБД.

  • cuba.dataSourceProvider - значение jndi указывает, что источник данных должен быть получен из JNDI.

JNDI-имя источника данных задается свойством cuba.dataSourceJndiName, которое по умолчанию имеет значение java:comp/env/jdbc/CubaDS. Для дополнительных хранилищ необходимо задать свойство с таким же именем, но с добавлением имени хранилища.

Например:

# main data store connection parameters
cuba.dbmsType = hsql
cuba.dataSourceProvider = jndi

# names of additional data stores
cuba.additionalStores = clients,orders

# 'clients' data store connection parameters
cuba.dbmsType_clients = postgres
cuba.dataSourceProvider_clients = jndi
cuba.dataSourceJndiName_clients = jdbc/ClientsDS

# 'orders' data store connection parameters
cuba.dbmsType_orders = mssql
cuba.dataSourceProvider_orders = jndi
cuba.dataSourceJndiName_orders = jdbc/OrdersDS

Кроме того, для каждого хранилища необходимо в файле spring.xml модуля core задать определение бина CubaDataSourceFactoryBean с соответствующими параметрами storeName и jndiNameAppProperty. Например:

<bean id="cubaDataSource_clients" class="com.haulmont.cuba.core.sys.CubaDataSourceFactoryBean">
    <property name="storeName" value="clients"/>
    <property name="jndiNameAppProperty" value="cuba.dataSourceJndiName_clients"/>
</bean>

<bean id="cubaDataSource_orders" class="com.haulmont.cuba.core.sys.CubaDataSourceFactoryBean">
    <property name="storeName" value="orders"/>
    <property name="jndiNameAppProperty" value="cuba.dataSourceJndiName_orders"/>
</bean>

Источники данных, получаемые из JNDI конфигурируются специфичным для используемого сервера приложения способом. Для Tomcat это делается в файле context.xml. CUBA Studio записывает параметры соединения в файл modules/core/web/META-INF/context.xml и использует его в процессе стандартного развертывания при разработке приложения.

Если источник данных сконфигурирован в context.xml, задачи миграции БД должны иметь собственные параметры подключения к БД, например:

task createDb(dependsOn: assembleDbScripts, description: 'Creates local database', type: CubaDbCreation) {
    dbms = 'hsql'
    host = 'localhost:9111'
    dbName = 'demo'
    dbUser = 'sa'
    dbPassword = ''
}

task updateDb(dependsOn: assembleDbScripts, description: 'Updates local database', type: CubaDbUpdate) {
    dbms = 'hsql'
    host = 'localhost:9111'
    dbName = 'demo'
    dbUser = 'sa'
    dbPassword = ''
}
3.3.1.1. Использование произвольной схемы БД

PostgreSQL и Microsoft SQL Server поддерживают подключение к произвольной схеме внутри базы данных. По умолчанию на PostgreSQL используется схема public, на SQL Server - схема dbo.

PostgreSQL

При использовании Studio, добавьте параметр currentSchema в поле Connection params окна Data Store Properties. При этом Studio автоматически обновит файлы проекта в соответствии с выбранным способом конфигурации источника данных. В противном случае, укажите параметр соединения как описано ниже.

Если источник данных конфигурируется в приложении, добавьте свойство с полным URL подключения, например:

cuba.dataSource.jdbcUrl = jdbc:postgresql://localhost/demo?currentSchema=my_schema

Если источник данных получается из JNDI, добавьте параметр currentSchema в URL подключения в определении источника данных (для Tomcat оно находится в context.xml) и в свойство connectionParams задач сборки createDb и updateDb, например:

task createDb(dependsOn: assembleDbScripts, type: CubaDbCreation) {
    dbms = 'postgres'
    host = 'localhost'
    dbName = 'demo'
    connectionParams = '?currentSchema=my_schema'
    dbUser = 'postgres'
    dbPassword = 'postgres'
}

После этого можно запускать обновление или пересоздание БД, все таблицы будут созданы в указанной схеме.

Microsoft SQL Server

На Microsoft SQL Server указания параметра подключения недостаточно, необходимо также создать связь между пользователем БД и схемой. Ниже приведен пример создания базы данных и использования схемы.

  • Создайте login:

    create login JohnDoe with password='saPass1'
  • Создайте новую БД:

    create database my_db
  • Подключитесь к новой БД как sa, создайте схему, затем создайте пользователя и дайте ему права владельца:

    create schema my_schema
    
    create user JohnDoe for login JohnDoe with default_schema = my_schema
    
    exec sp_addrolemember 'db_owner', 'JohnDoe'

При использовании Studio, добавьте параметр currentSchema в поле Connection params окна Data Store Properties. При этом Studio автоматически обновит файлы проекта в соответствии с выбранным способом конфигурации источника данных. В противном случае, укажите параметр соединения как описано ниже.

Если источник данных конфигурируется в приложении, добавьте свойство с полным URL подключения, например:

cuba.dataSource.jdbcUrl = jdbc:sqlserver://localhost;databaseName=demo;currentSchema=my_schema

Если источник данных получается из JNDI, добавьте параметр currentSchema в URL подключения в определении источника данных (для Tomcat оно находится в context.xml) и в свойство connectionParams задач сборки createDb и updateDb, например:

task updateDb(dependsOn: assembleDbScripts, type: CubaDbUpdate) {
    dbms = 'mssql'
    dbmsVersion = '2012'
    host = 'localhost'
    dbName = 'demo'
    connectionParams = ';currentSchema=my_schema'
    dbUser = 'JohnDoe'
    dbPassword = 'saPass1'
}

Имейте в виду, что пересоздавать БД SQL Server из Studio или выполнением createDb в командной строке нельзя, так как использование не-дефолтной схемы требует ассоциации с пользователем. Однако можно выполнять Update Database в Studio или updateDb в командной строке, и все необходимые таблицы будут созданы в существующей базе данных и в указанной схеме.

3.3.2. Типы СУБД

Тип используемой СУБД определяется свойствами приложения cuba.dbmsType и (опционально) cuba.dbmsVersion, которые влияют на поведение механизмов, зависящих от типа базы данных.

Платформа "из коробки" поддерживает следующие СУБД:

cuba.dbmsType cuba.dbmsVersion JDBC driver

HSQLDB

hsql

org.hsqldb.jdbc.JDBCDriver

PostgreSQL 8.4+

postgres

org.postgresql.Driver

Microsoft SQL Server 2005

mssql

2005

net.sourceforge.jtds.jdbc.Driver

Microsoft SQL Server 2008

mssql

com.microsoft.sqlserver.jdbc.SQLServerDriver

Microsoft SQL Server 2012+

mssql

2012

com.microsoft.sqlserver.jdbc.SQLServerDriver

Oracle Database 11g+

oracle

oracle.jdbc.OracleDriver

MySQL 5.6+

mysql

com.mysql.jdbc.Driver

MariaDB 5.5+

mysql

org.mariadb.jdbc.Driver

Таблица ниже описывает рекомендованное соответствие типов данных между атрибутами сущностей в Java и колонками таблиц различных СУБД. Эти типы автоматически выбираются Studio при генерации скриптов создания и обновления БД, и для них гарантируется работоспособность всех механизмов платформы.

Java HSQL PostgreSQL MS SQL Server Oracle MySQL MariaDB

UUID

varchar(36)

uuid

uniqueidentifier

varchar2(32)

varchar(32)

varchar(32)

Date

timestamp

timestamp

datetime

timestamp

datetime(3)

datetime(3)

java.sql.Date

timestamp

date

datetime

date

date

date

java.sql.Time

timestamp

time

datetime

timestamp

time(3)

time(3)

BigDecimal

decimal(p, s)

decimal(p, s)

decimal(p, s)

number(p, s)

decimal(p, s)

decimal(p, s)

Double

double precision

double precision

double precision

float

double precision

double precision

Long

bigint

bigint

bigint

number(19)

bigint

bigint

Integer

integer

integer

integer

integer

integer

integer

Boolean

boolean

boolean

tinyint

char(1)

boolean

boolean

String (limited)

varchar(n)

varchar(n)

varchar(n)

varchar2(n)

varchar(n)

varchar(n)

String (unlimited)

longvarchar

text

varchar(max)

clob

longtext

longtext

byte[]

longvarbinary

bytea

image

blob

longblob

longblob

Как правило, всю работу по преобразованию данных между БД и кодом Java выполняет слой ORM совместно с соответствующим JDBC драйвером. Это означает, что при работе с данными через DataManager, EntityManager и запросы на JPQL никакой ручной конвертации выполнять не нужно - вы просто используете типы Java, перечисленные в левой колонке таблицы.

При использовании native SQL через EntityManager.createNativeQuery() или через QueryRunner для разных типов СУБД некоторые типы данных в Java коде будут отличаться от приведенных. В первую очередь это касается атрибутов типа UUID - только драйвер PostgreSQL возвращает значения соответствующих колонок в этом типе, для других серверов это будет String. Для обеспечения независимости кода от используемой СУБД рекомендуется конвертировать типы параметров и результатов запросов с помощью интерфейса DbTypeConverter.

3.3.2.1. Поддержка произвольных СУБД

На уровне прикладного проекта можно реализовать работу с любой СУБД, поддерживаемой фреймворком ORM (EclipseLink). Для этого достаточно выполнить следующее:

  • Указать тип СУБД в виде произвольного кода в свойстве cuba.dbmsType. Код должен отличаться от используемых в платформе кодов hsql, postgres, mssql, oracle.

  • Реализовать интерфейсы DbmsFeatures, SequenceSupport, DbTypeConverter классами с именами соответственно TypeDbmsFeatures, TypeSequenceSupport, TypeDbTypeConverter, где Type - код типа СУБД. Пакет класса имплементации должен быть таким же, как у интерфейса.

  • Если источник данных сконфигурирован в приложении, необходимо указать полный URL соединения в требуемом СУБД формате, как описано в разделе Подключение к базам данных.

  • Создать скрипты инициализации и обновления БД в каталогах с кодом СУБД. Скрипты инициализации должны включать создание всех объектов БД, необходимых для сущностей платформы (их можно скопировать из имеющихся в каталоге 10-cuba и др. скриптов и исправить для данной СУБД).

  • Для создания и обновления БД задачами Gradle в build.gradle необходимо для этих задач указать дополнительные параметры:

    task createDb(dependsOn: assemble, type: CubaDbCreation) {
        dbms = 'my'                                            // DBMS code
        driver = 'net.my.jdbc.Driver'                          // JDBC driver class
        dbUrl = 'jdbc:my:myserver://192.168.47.45/mydb'        // Database URL
        masterUrl = 'jdbc:my:myserver://192.168.47.45/master'  // URL of a master DB to connect to for creating the application DB
        dropDbSql = 'drop database mydb;'                      // Drop database statement
        createDbSql = 'create database mydb;'                  // Create database statement
        timeStampType = 'datetime'                             // Date and time datatype - needed for SYS_DB_CHANGELOG table creation
        dbUser = 'sa'
        dbPassword = 'saPass1'
    }
    
    task updateDb(dependsOn: assemble, type: CubaDbUpdate) {
        dbms = 'my'                                            // DBMS code
        driver = 'net.my.jdbc.Driver'                          // JDBC driver class
        dbUrl = 'jdbc:my:myserver://192.168.47.45/mydb'        // Database URL
        dbUser = 'sa'
        dbPassword = 'saPass1'
    }

Также поддержку произвольной СУБД можно добавить и в CUBA Studio. После реализации интеграции Studio позволит разработчику использовать стандартные диалоги для изменения настроек хранилища данных. А также (самое важное) Studio сможет автоматически генерировать скрипты миграции БД для сущностей, как определенных в проекте, так и унаследованных из платформы и аддонов. Инструкции по интеграции доступны в соответствующей главе руководства пользователя Studio.

Работающий пример встраивания сторонней СУБД (Firebird) в платформу CUBA platform и CUBA Studio находится здесь: https://github.com/cuba-labs/firebird-sample.

3.3.2.2. Версия СУБД

В дополнение к свойству приложения cuba.dbmsType существует опциональное свойство cuba.dbmsVersion. Оно влияет на выбор имплементаций интерфейсов DbmsFeatures, SequenceSupport, DbTypeConverter, и на поиск скриптов создания и обновления БД.

Имя класса имплементации интеграционного интерфейса формируется следующим образом: TypeVersionName. Здесь Type - значение cuba.dbmsType с заглавной буквы, Version - значение cuba.dbmsVersion, Name - имя интерфейса. Пакет класса должен быть таким же, как у интерфейса. Если класс с таким именем отсутствует, предпринимается попытка найти класс с именем без версии: TypeName. Если и такого класса нет, выдается исключение.

Например, в платформе определен класс com.haulmont.cuba.core.sys.persistence.Mssql2012SequenceSupport, который вступит в силу, если в проекте указаны следующие свойства:

cuba.dbmsType = mssql
cuba.dbmsVersion = 2012

При поиске скриптов создания и обновления БД каталог с именем type-version имеет приоритет над каталогом с именем type. Это значит, что скрипты каталога type-version заменяют одноименные скрипты каталога type. В каталоге type-version могут быть и скрипты с собственными именами, они будут также добавлены в общий набор скриптов для выполнения. Сортировка скриптов производится по пути начиная с каталога, вложенного в type или type-version, то есть без учета того, в каком каталоге (с версией или без) находится скрипт.

Например, следующим образом можно определить скрипты создания БД для Microsoft SQL Server для версий ниже и выше 2012:

modules/core/db/init/
   mssql/
       10.create-db.sql
       20.create-db.sql
       30.create-db.sql
   mssql-2012/
       10.create-db.sql
3.3.2.3. Особенности MS SQL Server

Microsoft SQL Server использует кластерные индексы для таблиц.

По умолчанию кластерный индекс создается по первичному ключу таблицы, однако используемые в CUBA-приложении ключи типа UUID плохо подходят для кластерного индекса. Мы рекомендуем создавать первичные ключи типа UUID с модификатором nonclustered:

create table SALES_CUSTOMER (
    ID uniqueidentifier not null,
    CREATE_TS datetime,
    ...
    primary key nonclustered (ID)
)^
3.3.2.4. Особенности MySQL

Если в вашей базе данных используется кодировка, отличная от UTF-8, некоторые скрипты фреймворка не смогут быть выполнены. В этом случае необходимо изменить свойства базы данных Charset и Collation name, передав следующие параметры подключения в поле Connection params окна Data Store Properties:

?useUnicode=true&characterEncoding=UTF-8

MySQL не поддерживает частичные (partial) индексы, поэтому единственная возможность создать ограничение уникальности для soft deleted сущности - это использовать в составе индекса колонку DELETE_TS. Однако, существует другая проблема: MySQL позволяет иметь несколько NULLs в колонке с ограничением уникальности. Так как стандартная колонка DELETE_TS является nullable, она не может быть использована в уникальном индексе. Рекомендуется следующий способ создания уникальных ограничений для сущностей с мягким удалением:

  1. Создайте в таблице колонку DELETE_TS_NN с параметром not null и значением по умолчанию:

    create table DEMO_CUSTOMER (
        ...
        DELETE_TS_NN datetime(3) not null default '1000-01-01 00:00:00.000',
        ...
    )
  2. Создайте триггер, изменяющий DELETE_TS_NN когда меняется DELETE_TS:

    create trigger DEMO_CUSTOMER_DELETE_TS_NN_TRIGGER before update on DEMO_CUSTOMER
    for each row
        if not(NEW.DELETE_TS <=> OLD.DELETE_TS) then
            set NEW.DELETE_TS_NN = if (NEW.DELETE_TS is null, '1000-01-01 00:00:00.000', NEW.DELETE_TS);
        end if
  3. Создайте уникальный индекс, включающий в себя уникальные колонки и DELETE_TS_NN:

    create unique index IDX_DEMO_CUSTOMER_UNIQ_NAME on DEMO_CUSTOMER (NAME, DELETE_TS_NN)

3.3.3. Скрипты миграции БД

Проект CUBA-приложения всегда содержит два набора скриптов:

  • Скрипты создания БД, предназначенные для создания базы данных с нуля. Они содержат набор DDL и DML операторов, после выполнения которых на пустой БД схема базы данных полностью соответствует текущему состоянию модели данных приложения. Скрипты создания могут также наполнять БД необходимыми первичными данными.

  • Скрипты обновления БД - предназначены для поэтапного приведения структуры БД к текущему состоянию модели данных.

CUBA Studio автоматически создает скрипты миграции БД для изменяющейся модели данных вашего проекта, см. документацию. Информация, приведенная ниже, может быть полезна для лучшего понимания данного процесса, а также при создании Groovy-скриптов миграции, которые не поддерживаются в Studio.

При изменении модели данных необходимо отразить соответствующее изменение схемы БД и в скриптах создания, и в скриптах обновления. Например, при добавлении атрибута address в сущность Customer, нужно:

  1. Изменить оператор создания таблицы в скрипте создания:

    create table SALES_CUSTOMER (
      ID varchar(36) not null,
      CREATE_TS timestamp,
      CREATED_BY varchar(50),
      NAME varchar(100),
      ADDRESS varchar(200), -- added column
      primary key (ID)
    )
  2. Добавить скрипт обновления, содержащий оператор модификации таблицы:

    alter table SALES_CUSTOMER add ADDRESS varchar(200)

    Обратите внимание, что генератор скриптов Studio не отслеживает изменения Column definition атрибутов сущностей и sqlType специализированных datatype. Таким образом, при их изменении соответствующие скрипты обновления необходимо создавать вручную.

Скрипты создания располагаются в каталоге /db/init модуля core. Скрипты создания дополнительных хранилищ (если таковые необходимы) должны располагаться в каталогах вида /db/init_<datastore_name_in_lower_case>. Для каждого типа СУБД, поддерживаемой приложением, создается свой набор скриптов и располагается в подкаталоге с именем, соответствующим свойству приложения cuba.dbmsType, например, /db/init/postgres. Имена скриптов создания должны иметь вид {optional_prefix}create-db.sql.

Скрипты обновления располагаются в каталоге /db/update модуля core. Скрипты обновления дополнительных хранилищ (если таковые необходимы) должны располагаться в каталогах вида /db/update_<datastore_name_in_lower_case>. Для каждого типа СУБД, поддерживаемой приложением, создается свой набор скриптов и располагается в подкаталоге с именем, соответствующим свойству приложения cuba.dbmsType, например, /db/update/postgres.

Скрипты обновления могут быть двух типов: с расширением *.sql или с расширением *.groovy. SQL-скрипты являются основным средством обновления базы данных. Groovy-скрипты выполняются только механизмом запуска скриптов БД сервером, поэтому применяются в основном на этапе эксплуатации приложения - как правило, это процессы миграции или импорта данных, которые невозможно реализовать на SQL. Чтобы пропустить скрипты Groovy при обновлении, вы можете выполнить следующую команду из командной строки:

delete from sys_db_changelog where script_name like '%groovy' and create_ts > (now() - interval '1 hour')

Скрипты обновления должны иметь имена, которые при сортировке в алфавитном порядке образуют правильную последовательность их выполнения (обычно это хронологическая последовательность их создания). Поэтому при ручном создании рекомендуется задавать имя скрипта обновления в виде {yymmdd}-{description}.sql, где yy - год, mm - месяц, dd - день, description - краткое описание скрипта. Например, 121003-addCodeToCategoryAttribute.sql. Studio при автоматической генерации скриптов также придерживается этого формата.

Чтобы groovy-скрипты также выполнялись командой updateDb, они обязательно должны иметь расширение .upgrade.groovy и ту же логику именования, что и sql-скрипты. Действия из набора Post update в этих скриптах не поддерживаются, для привязки к данным используются стандартные переменные ds (доступ к источнику данных) и log (доступ к журналу сервера). Выполнение groovy-скриптов можно запретить, прописав executeGroovy = false в команде updateDb в build.gradle.

Скрипты обновления можно группировать в подкаталоги, главное, чтобы путь к скрипту с учетом подкаталога не нарушал хронологической последовательности. Например, можно создавать подкаталоги по номеру года или по году и месяцу.

В развернутом приложении скрипты создания и обновления БД располагаются в специальном каталоге скриптов базы данных, задаваемым свойством приложения cuba.dbDir.

3.3.3.1. Структура SQL-скриптов

SQL-скрипты миграции представляют собой текстовые файлы с набором DDL и DML команд, разделенных символом “^”. Символ “^” применяется для того, чтобы можно было применять разделитель “;” в составе сложных команд, например, при создании функций или триггеров. Механизм исполнения скриптов разделяет входной файл на команды по разделителю “^” и выполняет каждую команду в отдельной транзакции. Это означает, что при необходимости можно сгруппировать несколько простых операторов (например, insert), разделенных точкой с запятой, и обеспечить их выполнение в одной транзакции.

Разделитель "^" может быть заэкранирован путем его удвоения. Например, если вам нужно передать значение ^[0-9\s]+$, скрипт должен содержать строку ^^[0-9\s]+$.

Пример SQL-скрипта обновления:

create table LIBRARY_COUNTRY (
  ID varchar(36) not null,
  CREATE_TS time,
  CREATED_BY varchar(50),
  NAME varchar(100) not null,
  primary key (ID)
)^

alter table LIBRARY_TOWN add column COUNTRY_ID varchar(36) ^
alter table LIBRARY_TOWN add constraint FK_LIBRARY_TOWN_COUNTRY_ID foreign key (COUNTRY_ID) references LIBRARY_COUNTRY(ID)^
create index IDX_LIBRARY_TOWN_COUNTRY on LIBRARY_TOWN (COUNTRY_ID)^
3.3.3.2. Структура Groovy-скриптов

Groovy-скрипты обновления имеют следующую структуру:

  • Основная часть, содержащая код, выполняемый до старта контекста приложения. В этой части можно использовать любые классы Java, Groovy и блока Middleware приложения, но при этом необходимо иметь в виду, что никакие бины, интерфейсы инфраструктуры и прочие объекты приложения еще не инстанциированы, и с ними работать нельзя.

    Основная часть предназначена в первую очередь, как и обычные SQL-скрипты, для обновления схемы данных.

  • PostUpdate часть - набор замыканий, которые будут выполнены после завершения процесса обновления и после старта контекста приложения. Внутри этих замыканий можно оперировать любыми объектами Middleware приложения.

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

На вход Groovy-скриптов механизм выполнения передает следующие переменные:

  • ds - экземпляр javax.sql.DataSource для базы данных приложения.

  • log - экземпляр org.apache.commons.logging.Log для вывода сообщений в журнал сервера

  • postUpdate - объект, содержащий метод add(Closure closure) для добавления замыканий, выполняющихся после старта контекста сервера.

Groovy-скрипты выполняются только механизмом запуска скриптов БД сервером.

Пример Groovy-скрипта обновления:

import com.haulmont.cuba.core.Persistence
import com.haulmont.cuba.core.global.AppBeans
import com.haulmont.refapp.core.entity.Colour
import groovy.sql.Sql

log.info('Executing actions in update phase')

Sql sql = new Sql(ds)
sql.execute """ alter table MY_COLOR add DESCRIPTION varchar(100); """

// Add post update action
postUpdate.add({
    log.info('Executing post update action using fully functioning server')

    def p = AppBeans.get(Persistence.class)
    def tr = p.createTransaction()
    try {
        def em = p.getEntityManager()

        Colour c = new Colour()
        c.name = 'yellow'
        c.description = 'a description'

        em.persist(c)
        tr.commit()
    } finally {
        tr.end()
    }
})
3.3.3.3. Выполнение скриптов БД задачами Gradle

Данный механизм применяется обычно разработчиками приложения для собственного экземпляра базы данных. Выполнение скриптов в этом случае сводится к запуску специальных задач Gradle, описанных в скрипте сборки build.gradle. Это можно сделать как из командной строки, так и с помощью интерфейса Studio.

Для запуска скриптов создания БД служит задача createDb. В Studio ей соответствует команда главного меню CUBACreate database. При запуске задачи происходит следующее:

  1. В каталоге modules/core/build/db собираются скрипты компонентов платформы и скрипты db/**/*.sql модуля core текущего проекта. Наборы скриптов располагаются в подкаталогах с числовыми префиксами. Числовые префиксы необходимы для соблюдения алфавитного порядка выполнения скриптов - сначала выполняются скрипты cuba, затем других компонентов, затем текущего проекта.

  2. Если БД существует, она полностью очищается. Если не существует, то создается новая пустая БД.

  3. Последовательно в алфавитном порядке выполняются все скрипты создания modules/core/build/db/init/**/*create-db.sql, и их имена вместе с путем относительно каталога db регистрируются в таблице SYS_DB_CHANGELOG.

  4. В таблице SYS_DB_CHANGELOG аналогично регистрируются все имеющиеся на данный момент скрипты обновления modules/core/build/db/update/**/*.sql. Это необходимо для будущего инкрементального обновления БД новыми скриптами.

Для запуска скриптов обновления БД служит задача updateDb. В Studio ей соответствует команда главного меню CUBA > Update Database. При запуске задачи происходит следующее:

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

  2. Производится проверка, выполнены ли все скрипты создания схемы компонентов приложения (путем выборки из таблицы SYS_DB_CHANGELOG). Если обнаруживается, что БД не инициализирована для работы некоторого компонента, выполняются его скрипты создания.

  3. В каталогах modules/core/build/db/update/** производится поиск скриптов обновления, не зарегистрированных в таблице SYS_DB_CHANGELOG, то есть не выполненных ранее и содержимое которых не отражено в БД при ее инициализации.

  4. Последовательно в алфавитном порядке выполняются все найденные на предыдущем шаге скрипты, и их имена вместе с путем относительно каталога db регистрируются в таблице SYS_DB_CHANGELOG.

3.3.3.4. Выполнение скриптов БД сервером

Механизм выполнения скриптов сервером предназначен для приведения БД в актуальное состояние на старте сервера приложения, и активируется во время инициализации блока Middleware. Понятно, что при этом приложение должно быть собрано и развернуто на сервере, будь то собственный Tomcat разработчика или сервер в режиме эксплуатации.

Данный механизм в зависимости от описанных ниже условий выполняет либо скрипты создания, либо скрипты обновления, то есть он может и инициализировать БД с нуля, и обновлять ее. Однако, в отличие от описанной в предыдущем разделе задачи Gradle createDb, для выполнения инициализации базы она должна существовать - сервер не создает БД автоматически, а только прогоняет на ней скрипты.

Механизм выполнения скриптов сервером действует следующим образом:

  • Скрипты читаются из каталога скриптов базы данных, определяемого свойством приложения cuba.dbDir, установленного по умолчанию в WEB-INF/db.

  • Если в БД отсутствует таблица SEC_USER, то считается, что база данных пуста, и запускается полная инициализация с помощью скриптов создания БД. После выполнения инициализирующих скриптов их имена запоминаются в таблице SYS_DB_CHANGELOG. Кроме того, там же сохраняются имена всех доступных скриптов обновления, без их выполнения.

  • Если в БД имеется таблица SEC_USER, но отсутствует таблица SYS_DB_CHANGELOG (это случай, когда в первый раз запускается описываемый механизм на имеющейся рабочей БД), никакие скрипты не запускаются. Вместо этого создается таблица SYS_DB_CHANGELOG и в ней сохраняются имена всех доступных на данный момент скриптов создания и обновления.

  • Если в БД имеются и таблица SEC_USER и таблица SYS_DB_CHANGELOG, то производится запуск скриптов обновления, и их имена запоминаются в таблице SYS_DB_CHANGELOG. Причем запускаются только те скрипты, имен которых до этого не было в таблице SYS_DB_CHANGELOG, т.е. не запускавшиеся ранее. Последовательность запуска скриптов определяется двумя факторами: приоритетом базового проекта (см. содержимое каталога скриптов базы данных: 10-cuba, 20-workflow, …​) и именем файла скрипта (с учетом подкаталогов внутри каталога update) в алфавитном порядке.

    Перед выполнением скриптов обновления производится проверка, все ли скрипты создания схемы компонентов приложения выполнены (путем выборки из таблицы SYS_DB_CHANGELOG). Если обнаруживается, что БД не инициализирована для работы некоторого компонента, выполняются его скрипты создания.

Механизм выполнения скриптов на старте сервера включается свойством приложения cuba.automaticDatabaseUpdate.

В запущенном приложении механизм выполнения скриптов можно стартовать с помощью JMX-бина app-core.cuba:type=PersistenceManager, вызвав его метод updateDatabase() с параметром update. Понятно, что таким способом можно только обновить БД, а не проинициализировать новую, так как войти в систему для запуска метода JMX-бина при пустой БД невозможно. При этом следует иметь в виду, что если на старте Middleware или при входе пользователя в систему начнется инициализация той части модели данных, которая уже не соответствует устаревшей схеме БД, то произойдет ошибка, и продолжение работы станет невозможным. Именно поэтому универсальным является только автоматическое обновление БД на старте сервера перед инициализацией модели данных.

JMX-бин app-core.cuba:type=PersistenceManager имеет еще один метод, относящийся к механизму обновления БД: findUpdateDatabaseScripts(). Он возвращает список новых скриптов обновления, имеющихся в каталоге и не зарегистрированных в БД.

Практические рекомендации по использованию механизма обновления БД сервером приведены в Создание и обновление БД при эксплуатации приложения.

3.4. Компоненты среднего слоя

На следующем рисунке приведены основные компоненты среднего слоя CUBA-приложения.

Middleware
Рисунок 8. Компоненты среднего слоя

ServicesSpring-бины, формирующие границу приложения и предоставляющие интерфейс клиентскому уровню приложения. Сервисы могут содержать бизнес-логику сами, либо делегировать выполнение managed beans.

Managed Beans – Spring-бины, содержащие бизнес-логику приложения. Вызываются сервисами, другими бинами или через опциональный JMX-интерфейс.

Persistence − инфраструктурный интерфейс для доступа к функциональности хранения данных: управлению транзакциями и ORM.

3.4.1. Сервисы

Сервисы образуют слой компонентов, определяющий множество операций Middleware, доступных клиентскому уровню приложения. Другими словами, сервис представляет собой точку входа в бизнес-логику среднего слоя. В сервисе можно управлять транзакциями, проверять права пользователей, работать с базой данных или делегировать выполнение другим Spring-бинам среднего слоя.

Диаграмма классов сервиса, отображающая основные компоненты сервиса:

MiddlewareServices

Интерфейс сервиса располагается в модуле global и доступен на клиентском уровне и на Middleware. Во время выполнения, на клиентском уровне для интерфейса сервиса создается прокси-объект, который обеспечивает вызовы методов бина, реализующего сервис, с помощью механизма Spring HTTP Invoker.

Бин, реализующий сервис, располагается в модуле core и доступен только на среднем слое.

ServiceInterceptor автоматически вызывается для каждого метода сервиса через Spring AOP. Он проверяет наличие в потоке выполнения пользовательской сессии, а также трансформирует и логирует исключения, если сервис вызван с клиентского уровня.

3.4.1.1. Создание сервиса

Руководство Create Business Logic in CUBA демонстрирует использование среднего слоя для реализации бизнес-логики.

Имена интерфейсов сервисов должны заканчиваться на Service, имена классов реализации на ServiceBean.

CUBA Studio позволяет легко создавать заглушки интерфейса и класса сервисов, а также автоматически регистрирует созданные сервисы в spring.xml. Для создания сервиса используйте команду New > Service в контекстном меню секции Middleware проектного дерева CUBA Studio.

При создании сервиса вручную без Studio необходимо выполнить следующее:

  1. Создать интерфейс в модуле global (т.к. интерфейс сервиса должен быть доступен на всех уровнях) и задать в нем имя сервиса. Имя рекомендуется задавать в формате {имя_проекта}_{имя_интерфейса}. Например:

    package com.sample.sales.core;
    
    import com.sample.sales.entity.Order;
    
    public interface OrderService {
        String NAME = "sales_OrderService";
    
        void calculateTotals(Order order);
    }
  2. Создать класс сервиса в модуле core и добавить ему аннотацию @org.springframework.stereotype.Service с именем, заданным в интерфейсе

    package com.sample.sales.core;
    
    import com.sample.sales.entity.Order;
    import org.springframework.stereotype.Service;
    
    @Service(OrderService.NAME)
    public class OrderServiceBean implements OrderService {
        @Override
        public void calculateTotals(Order order) {
        }
    }

Класс сервиса, как и класс любого другого Spring-бина, должен находиться внутри дерева пакетов с корнем, заданным в элементе context:component-scan файла spring.xml. В нашем случае файл spring.xml содержит элемент:

<context:component-scan base-package="com.sample.sales"/>

что означает, что поиск аннотированных бинов для данного блока приложения будет происходить начиная с пакета com.sample.sales.

Если некоторую бизнес-логику требуется вызывать из разных сервисов либо других компонентов Middleware, ее необходимо выделить и инкапсулировать внутри соответствующего Spring-бина. Например:

// service interface
public interface SalesService {
    String NAME = "sample_SalesService";

    BigDecimal calculateSales(UUID customerId);
}
// service implementation
@Service(SalesService.NAME)
public class SalesServiceBean implements SalesService {

    @Inject
    private SalesCalculator salesCalculator;

    @Transactional
    @Override
    public BigDecimal calculateSales(UUID customerId) {
        return salesCalculator.calculateSales(customerId);
    }
}
// managed bean encapsulating business logic
@Component
public class SalesCalculator {

    @Inject
    private Persistence persistence;

    public BigDecimal calculateSales(UUID customerId) {
        Query query = persistence.getEntityManager().createQuery(
                "select sum(o.amount) from sample_Order o where o.customer.id = :customerId");
        query.setParameter("customerId", customerId);
        return (BigDecimal) query.getFirstResult();
    }
}
3.4.1.2. Использование сервиса

Для того чтобы вызывать сервис, в клиентском блоке приложения для него должен быть создан соответствующий прокси-объект. Делается это путем объявления имени и интерфейса сервиса в параметрах фабрики прокси-объектов. Для блока Web Client это бин класса WebRemoteProxyBeanCreator, для Web Portal - PortalRemoteProxyBeanCreator.

Фабрика прокси-объектов конфигурируется в файле spring.xml соответствующего клиентского блока.

Например, чтобы в приложении sales вызвать с веб-клиента сервис sales_OrderService, необходимо добавить в файл web-spring.xml модуля web следующее:

<bean id="sales_proxyCreator" class="com.haulmont.cuba.web.sys.remoting.WebRemoteProxyBeanCreator">
    <property name="serverSelector" ref="cuba_ServerSelector"/>
    <property name="remoteServices">
        <map>
            <entry key="sales_OrderService" value="com.sample.sales.core.OrderService"/>
        </map>
    </property>
</bean>

Все импортируемые сервисы объявляются в одном свойстве remoteServices в элементах map/entry.

CUBA Studio автоматически регистрирует сервисы во всех клиентских блоках приложения.

С точки зрения прикладного кода прокси-объект сервиса на клиентском уровне является обычным бином Spring и может быть получен либо инжекцией, либо с помощью класса AppBeans, например:

@Inject
private OrderService orderService;

public void calculateTotals() {
    orderService.calculateTotals(order);
}
3.4.1.3. DataService

DataService является фасадом для вызова серверной реализации DataManager с клиентского уровня. DataService не рекомендуется использовать в прикладном коде. Вместо него и на клиентском уровне, и на Middleware следует использовать DataManager.

3.4.2. Data Stores

Предпочтительный способ работы с данными в CUBA-приложениях - использование сущностей: либо декларативно в связанных с данными визуальных компонентах, либо программно через DataManager или EntityManager. Сущности отображаются на данные в хранилище, которое обычно представляет собой реляционную БД. Приложение может работать с несколькими хранилищами, так что его модель данных будет содержать сущности, отображаемые на данные из разных БД.

Некоторая сущность может принадлежать только одному хранилищу. Сущности из разных хранилищ можно отображать на одном экране UI, при этом DataManager обеспечивает их чтение и запись в соответствующее хранилище. В зависимости от типа сущности, DataManager выбирает зарегистрированное хранилище, представленное реализацией интерфейса DataStore, и делегирует ему загрузку или сохранение. При управлении транзакциями и работе с сущностями через EntityManager необходимо явно указывать, какое хранилище использовать. Подробнее см. методы интерфейса Persistence и параметры аннотации @Transactional.

Платформа содержит единственную реализацию интерфейса DataStore: RdbmsStore, предназначенную для работы с реляционными СУБД через слой ORM. Кроме того, в проекте приложения можно реализовать DataStore для интеграции, например, с нереляционной СУБД или внешней системой, имеющей REST интерфейс.

Каждое CUBA-приложение имеет основное хранилище, которое содержит системные сущности и используется для входа пользователей в приложение. В данном руководстве под базой данных всегда имеется в виду основное хранилище, если явно не оговорено другое. Основное хранилище должно представлять собой реляционную БД, подключенную через источник данных JDBC. Дополнительное хранилище может быть любой реализацией интерфейса DataStore.

CUBA Studio позволяет настраивать дополнительные хранилища, см. документацию. Studio автоматически создает все необходимые свойства приложения и поддерживает соответствующие файлы persistence.xml. После настройки хранилища его можно указать в поле Data store редактора сущности. Кроме того, хранилище можно выбрать при запуске мастера Generate Model для создания сущностей, отображенных на существующую схему БД.

Ниже приведена информация, которая может быть полезна при поиске проблем или если Studio не используется.

Имена дополнительных хранилищ указываются в свойстве приложения cuba.additionalStores. Если дополнительное хранилище является реляционной БД (RdbmsStore), необходимо указать для него следующие свойства приложения:

  • cuba.dbmsType_<store_name> - тип базы данных хранилища.

  • cuba.persistenceConfig_<store_name> - путь к файлу persistence.xml хранилища.

  • cuba.dataSource…​ - параметры подключения как описано в разделе Подключение к базам данных.

Если вы реализовали интерфейс DataStore в проекте, имя бина реализации должно быть указано в свойстве приложения cuba.storeImpl_<store_name>.

Предположим, что в вашем проекте два дополнительных хранилища: db1 (база данных PostgreSQL) and mem1 (некоторое in-memory хранилище, реализованное бином проекта). Тогда необходимо указать следующие свойства приложения в файле app.properties модуля core:

cuba.additionalStores = db1, mem1

# RdbmsStore for Postgres database with data source obtained from JNDI
cuba.dbmsType_db1 = postgres
cuba.persistenceConfig_db1 = com/company/sample/db1-persistence.xml
cuba.dataSourceJndiName_db1 = jdbc/db1

# Custom store
cuba.storeImpl_mem1 = sample_InMemoryStore

Свойства cuba.additionalStores и cuba.persistenceConfig_db1 необходимо также указать в файлах свойств всех используемых блоков приложения (web-app.properties, portal-app.properties, и т.д.).

Ссылки между сущностями из разных хранилищ

DataManager может автоматически поддерживать TO-ONE ссылки между сущностями из разных хранилищ, если они объявлены нужным образом. Например, рассмотрим случай, когда необходимо в сущности Order, находящейся в главном хранилище, иметь ссылку на сущность Customer из дополнительного хранилища. Необходимо сделать следующее:

  • В сущности Order определить атрибут типа, соответствующего идентификатору Customer. Атрибут должен быть аннотирован как @SystemLevel чтобы исключить его из различных списков, доступных пользователям, в частности из атрибутов в Filter:

    @SystemLevel
    @Column(name = "CUSTOMER_ID")
    private Long customerId;
  • В сущности Order определить неперсистентный атрибут-ссылку на Customer и указать атрибут customerId как "related":

    @Transient
    @MetaProperty(related = "customerId")
    private Customer customer;
  • Включите неперсистентный атрибут customer в нужные представления.

После этого, когда Order будет загружаться с представлением, включающим атрибут customer, DataManager будет автоматически загружать связанные экземпляры Customer из дополнительного хранилища. Загрузка коллекций оптимизирована по производительности: после загрузки списка заказов загрузка покупателей из доп. хранилища производится пакетами. Размер пакета определяется свойством приложения cuba.crossDataStoreReferenceLoadingBatchSize.

При коммите графа объектов, включающего Order со ссылкой на Customer, DataManager сохранит сущности через соответствующие имплементации DataStore, а затем сохранит идентификатор Customer в атрибуте customerId сущности Order.

Ссылки между сущностями из разных хранилищ поддерживаются компонентом Filter.

CUBA Studio автоматически поддерживает набор атрибутов для ссылок между сущностями из разных хранилищ, если в качестве ассоциации выбирается сущность из другого хранилища.

3.4.3. Интерфейс Persistence

Интерфейс Persistence является точкой входа в функциональность хранения данных, предоставляемую слоем ORM.

Методы интерфейса:

  • createTransaction(), getTransaction() - получить интерфейс управления транзакциями. Методы могут принимать имя хранилища данных. Если хранилище не указано, подразумевается основная база данных.

  • callInTransaction(), runInTransaction() - выполнить код в новой транзакции с возвратом значения или без возврата значения. Методы могут принимать имя хранилища данных. Если хранилище не указано, подразумевается основная база данных.

  • isInTransaction() - определяет, существует ли в данный момент активная транзакция.

  • getEntityManager() - возвращает экземпляр EntityManager для текущей транзакции. Метод может принимать имя хранилища данных. Если хранилище не указано, подразумевается основная база данных.

  • isSoftDeletion() - позволяет определить, активен ли режим мягкого удаления

  • setSoftDeletion() - устанавливает или отключает режим мягкого удаления. Влияет на аналогичный признак всех создаваемых экземпляров EntityManager. По умолчанию мягкое удаление включено.

  • getDbTypeConverter() - возвращает экземпляр DbTypeConverter для основной базы данных или для дополнительного хранилища.

  • getDataSource() - получить javax.sql.DataSource для основной базы данных или для дополнительного хранилища.

    Для всех объектов javax.sql.Connection, получаемых методом getDataSource().getConnection(), необходимо после использования соединения вызвать метод close() в секции finally. В противном случае соединение не вернется в пул, через какое-то время пул переполнится, и приложение не сможет выполнять запросы к базе данных.

  • getTools() - возвращает экземпляр интерфейса PersistenceTools (см. ниже).

3.4.3.1. PersistenceTools

ManagedBean, содержащий вспомогательные методы работы с хранилищем данных. Интерфейс PersistenceTools можно получить либо методом Persistence.getTools(), либо как любой другой бин - инжекцией или через класс AppBeans.

Методы PersistenceTools:

  • getDirtyFields() - возвращает коллекцию имен атрибутов сущности, измененных со времени последней загрузки экземпляра из БД. Для новых экземпляров возвращает пустую коллекцию.

  • isLoaded() - определяет, загружен ли из БД указанный атрибут экземпляра. Атрибут может быть не загружен, если он не указан в примененном при загрузке представлении.

    Данный метод работает только для экземпляров в состоянии Managed.

  • getReferenceId() - возвращает идентификатор связанной сущности без загрузки ее из БД.

    Предположим, в персистентный контекст загружен экземпляр Order, и нужно получить значение идентификатора экземпляра Customer, связанного с данным Заказом. Стандартное решение order.getCustomer().getId() приведет к выполнению SQL запроса к БД для загрузки экземпляра Customer, что в данном случае избыточно, так как значение идентификатора Покупателя физически находится также и в таблице Заказов. Выполнение же

    persistence.getTools().getReferenceId(order, "customer")

не вызовет никаких дополнительных запросов к базе данных.

Данный метод работает только для экземпляров в состоянии Managed.

Для расширения набора вспомогательных методов в конкретном приложении бин PersistenceTools можно переопределить. Примеры работы с расширенным интерфейсом:

MyPersistenceTools tools = persistence.getTools();
tools.foo();
((MyPersistenceTools) persistence.getTools()).foo();
3.4.3.2. DbTypeConverter

Интерфейс, определяющий методы для конвертации данных между значениями атрибутов модели данных и параметрами и результатами запросов JDBC. Объект данного интерфейса можно получить методом Persistence.getDbTypeConverter().

Методы DbTypeConverter:

  • getJavaObject() - конвертирует результат JDBC запроса в тип, подходящий для присвоения атрибуту сущности.

  • getSqlObject() - конвертирует значение атрибута сущности в тип, подходящий для присвоения параметру JDBC запроса.

  • getSqlType() - возвращает константу из java.sql.Types, соответствующую переданному типу атрибута сущности.

3.4.4. Слой ORM

Object-Relational Mapping - объектно-реляционное отображение - технология связывания таблиц реляционной базы данных с объектами языка программирования. В платформе CUBA используется реализация ORM на основе фреймворка EclipseLink.

Использование ORM дает ряд очевидных преимуществ:

  • Позволяет работать с данными реляционной СУБД, манипулируя объектами Java.

  • Упрощает программирование, избавляя от рутины написания тривиальных SQL-запросов.

  • Упрощает программирование, позволяя извлекать и сохранять целые графы объектов одной командой.

  • Обеспечивает легкое портирование приложения на различные СУБД.

  • Позволяет использовать лаконичный язык запросов JPQL.

В то же время, имеются и некоторые недостатки. Во-первых, разработчик, использующий ORM непосредственно, должен хорошо понимать особенности работы этой технологии. Во-вторых, усложняется оптимизация SQL и использование особенности применяемой СУБД.

Если вы столкнулись с проблемами производительности при доступе к БД, начните с того, что проверьте, какие конкретно SQL-операторы выполняются в вашем приложении. Вы можете использовать логгер eclipselink.sql для вывода всех SQL-операторов, генерируемых ORM, в файл лога.

3.4.4.1. EntityManager

EntityManager - основной интерфейс ORM, служит для управления персистентными сущностями.

В разделе DataManager vs. EntityManager приведена информация о различиях между EntityManager и DataManager.

Ссылку на EntityManager можно получить через интерфейс Persistence, вызовом метода getEntityManager(). Полученный экземпляр EntityManager привязан к текущей транзакции, то есть все вызовы getEntityManager() в рамках одной транзакции возвращают один и тот же экземпляр EntityManager. После завершения транзакции обращения к данному экземпляру невозможны.

Экземпляр EntityManager содержит в себе персистентный контекст – набор экземпляров сущностей, загруженных из БД или только что созданных. Персистентный контекст является своего рода кэшем данных в рамках транзакции.EntityManager автоматически сбрасывает в БД все изменения, сделанные в его персистентном контексте, в момент коммита транзакции, либо при явном вызове метода flush().

Интерфейс EntityManager, используемый в CUBA-приложениях, в основном повторяет стандартный javax.persistence.EntityManager. Рассмотрим его основные методы:

  • persist() - вводит новый экземпляр сущности в персистентный контекст. При коммите транзакции командой SQL INSERT в БД будет создана соответствующая запись.

  • merge() - переносит состояние отсоединенного экземпляра сущности в персистентный контекст следующим образом: из БД загружается экземпляр с тем же идентификатором, в него переносится состояние переданного Detached экземпляра и возвращается загруженный Managed экземпляр. Далее надо работать именно с возвращенным Managed экземпляром. При коммите транзакции командой SQL UPDATE в БД будет сохранено состояние данного экземпляра.

  • remove() - удалить объект из базы данных, либо, если включен режим мягкого удаления, установить атрибуты deleteTs и deletedBy.

    Если переданный экземпляр находится в Detached состоянии, сначала выполняется merge().

  • find() - загружает экземпляр сущности по идентификатору.

    При формировании запроса к БД учитывается представление, переданное в параметре данного метода. В результате в персистентном контексте окажется граф объектов, для которого загружены все атрибуты представления. Если не передано никакого представления, по умолчанию будет использовано представление _local.

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

  • createQuery() - создать объект Query или TypedQuery для выполнения JPQL запроса.

  • createNativeQuery() - создать объект Query для выполнения SQL запроса.

  • addView() - аналогичен методу setView(), но в случае наличия уже установленного в EntityManager представления, не заменяет его, а добавляет атрибуты переданного представления.

  • reload() - перезагрузить экземпляр сущности с указанным представлением.

  • isSoftDeletion() - проверяет, находится ли данный EntityManager в режиме мягкого удаления.

  • setSoftDeletion() - устанавливает режим мягкого удаления для данного экземпляра EntityManager.

  • getConnection() - возвращает java.sql.Connection, через который выполняет запросы данный экземпляр EntityManager, и, соответственно, текущая транзакция. Закрывать такое соединение не нужно, оно будет закрыто при завершении транзакции.

  • getDelegate() - возвращает javax.persistence.EntityManager, предоставляемый реализацией ORM.

Пример использования EntityManager в сервисе:

@Service(SalesService.NAME)
public class SalesServiceBean implements SalesService {

    @Inject
    private Persistence persistence;

    @Override
    public BigDecimal calculateSales(UUID customerId) {
        BigDecimal result;
        // start transaction
        try (Transaction tx = persistence.createTransaction()) {
            // get EntityManager for the current transaction
            EntityManager em = persistence.getEntityManager();
            // create and execute Query
            Query query = em.createQuery(
                    "select sum(o.amount) from sample_Order o where o.customer.id = :customerId");
            query.setParameter("customerId", customerId);
            result = (BigDecimal) query.getFirstResult();
            // commit transaction
            tx.commit();
        }
        return result != null ? result : BigDecimal.ZERO;
    }
}
Частичные сущности

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

Вы можете заставить EntityManager загружать частичные сущности, если установите свойство loadPartialEntities представления в true (например, так делает DataManager). Однако, если загружаемая сущность кэшируется, то данный признак игнорируется, и сущность все равно будет загружена со всеми локальными атрибутами.

3.4.4.2. Состояния сущности
New

Только что созданный в памяти экземпляр, например: Car car = new Car(). Новый экземпляр может быть передан в EntityManager.persist() для сохранения в БД, при этом он переходит в состояние Managed.

Managed

Загруженный из БД или новый, переданный в EntityManager.persist(), экземпляр. Принадлежит некоторому экземпляру EntityManager, другими словами, находится в его персистентном контексте.

Любые изменения экземпляра в состоянии Managed будут сохранены в БД в случае коммита транзакции, к которой принадлежит данный EntityManager.

Detached

Экземпляр, загруженный из БД и отсоединенный от своего персистентного контекста (вследствие завершения транзакции или сериализации).

Изменения, вносимые в Detached экземпляр, запоминаются в самом этом экземпляре (в полях, добавленных с помощью bytecode enhancement). Эти изменения будут сохранены в БД, только если данный экземпляр будет снова переведен в состояние Managed путем передачи в метод EntityManager.merge().

3.4.4.3. Загрузка по требованию

Загрузка по требованию (lazy loading) позволяет загружать связанные сущности отложенно, т.е. только в момент первого обращения к их свойствам.

Загрузка по требованию в сумме порождает больше запросов к БД, чем жадная загрузка (eager fetching), однако нагрузка при этом растянута во времени.

  • Например, при извлечении списка N экземпляров сущности A, содержащих ссылку на экземпляр сущности B, в случае загрузки по требованию будет выполнено N+1 запросов к базе данных.

  • Как правило, для минимизации времени отклика и снижения нагрузки необходимо стремиться к меньшему количеству обращений к БД. Для этого в платформе используется механизм представлений, с помощью которого в вышеописанном случае ORM может сформировать один запрос к БД с объединением таблиц.

Загрузка по требованию работает только для экземпляра в состоянии Managed, то есть внутри транзакции, загрузившей данный экземпляр.

3.4.4.4. Выполнение JPQL запросов

В данном разделе описывается интерфейс Query, предназначенный для выполнения JPQL запросов на уровне ORM. Ссылку на Query можно получить у текущего экземпляра EntityManager вызовом метода createQuery(). Если запрос предполагается использовать для извлечения сущностей, рекомендуется вызывать createQuery() с передачей типа результата, что приведет к созданию TypedQuery.

Методы Query в основном соответствуют методам стандартного интерфейса JPA javax.persistence.Query. Рассмотрим отличия.

  • setView(), addView() - устанавливают представление, используемое при загрузке данных.

  • getDelegate() - возвращает экземпляр javax.persistence.Query, предоставляемый реализацией ORM.

Если для Query установлено представление, то по умолчанию Query имеет FlushModeType.AUTO, что влияет на случай, когда в текущем персистентном контексте содержатся измененные экземпляры сущностей: эти экземпляры будут сохранены в БД перед выполнением запроса. Другими словами, ORM сначала синхронизирует состояние сущностей в персистентном контексте и в БД, а уже потом выполняет запрос. Этим гарантируется, что в результаты запроса попадут все соответствующие экземпляры, даже если они еще не были сохранены в базе данных явно. Обратной стороной этого является неявный flush, т.е. выполнение команд SQL update для всех измененных в данном контексте сущностей, что может повлиять на производительность.

Если же Query выполняется без представления, то по умолчанию Query имеет FlushModeType.COMMIT, что означает, что неявный flush вызван не будет, и запрос не будет учитывать содержимое текущего персистентного контекста.

В большинстве случаев игнорирование текущего персистентного контекста допустимо, и является предпочтительным поведением, так как не вызывает дополнительных команд SQL. Однако, при использовании представлений существует следующая проблема: если в персистентном контексте есть измененный экземпляр сущности, и выполняется запрос с представлением и FlushModeType.COMMIT, загружающий этот же экземпляр, то изменения будут потеряны. Поэтому по умолчанию мы используем FlushModeType.AUTO для запросов с представлением.

Вы также можете явно установить flush mode с помощью метода setFlushMode() интерфейса Query, чтобы переопределить режим по умолчанию.

Using DELETE FROM with soft-deleted entities

Если в проекте используется мягкое удаление, то при выполнении JPQL-запроса DELETE FROM для сущности, удалённой через мягкое удаление, будет выброшено исключение. Дело в том, что такой запрос, по сути, будет трансформирован в запрос SQL для удаления всех сущностей, не помеченных для удаления. По умолчанию такое неочевидное поведение запрещено, но его можно разрешить с помощью свойства приложения cuba.enableDeleteStatementInSoftDeleteMode.

Query Hints

Метод Query.setHint() позволяет добавить некоторые подсказки (hints) в генерируемые команды SQL. Подсказки обычно используются для указания того, как запрос должен использовать индексы, или другую специфику СУБД. Фреймворк определяет следующие константы, которые можно использовать в качестве имен задаваемых подсказок:

  • QueryHints.SQL_HINT - значение подсказки будет добавлено после сгенерированной команды SQL. Указывайте полный текст подсказки, включая разделители комментариев, если они требуются.

  • QueryHints.MSSQL_RECOMPILE_HINT - будет добавлено выражение OPTION(RECOMPILE) для СУБД MS SQL Server. Значение подсказки при этом игнорируется.

При работе с DataManager подсказки в запрос можно передать, используя метод LoadContext.setHint().

3.4.4.4.1. Функции JPQL

В таблице ниже описаны функции JPQL, поддерживаемые и не поддерживаемые платформой CUBA.

Функция Поддерживается Пример запроса

Агрегатные функции

ДА

SELECT AVG(o.quantity) FROM app_Order o

НЕТ: агрегатные функции со скалярными выражениями (особенность EclipseLink)

SELECT AVG(o.quantity)/2.0 FROM app_Order o

SELECT AVG(o.quantity * o.price) FROM app_Order o

ALL, ANY, SOME

ДА

SELECT emp FROM app_Employee emp WHERE emp.salary > ALL (SELECT m.salary FROM app_Manager m WHERE m.department = emp.department)

Арифметические функции (INDEX, SIZE, ABS, SQRT, MOD)

ДА

SELECT w.name FROM app_Course c JOIN c.studentWaitlist w WHERE c.name = 'Calculus' AND INDEX(w) = 0

SELECT w.name FROM app_Course c WHERE c.name = 'Calculus' AND SIZE(c.studentWaitlist) = 1

SELECT w.name FROM app_Course c WHERE c.name = 'Calculus' AND ABS(c.time) = 10

SELECT w.name FROM app_Course c WHERE c.name = 'Calculus' AND SQRT(c.time) = 10.5

SELECT w.name FROM app_Course c WHERE c.name = 'Calculus' AND MOD(c.time, c.time1) = 2

CASE

ДА

SELECT e.name, f.name, CONCAT(CASE WHEN f.annualMiles > 50000 THEN 'Platinum ' WHEN f.annualMiles > 25000 THEN 'Gold ' ELSE '' END, 'Frequent Flyer') FROM app_Employee e JOIN e.frequentFlierPlan f

НЕТ: CASE в UPDATE-запросе

UPDATE app_Employee e SET e.salary = CASE e.rating WHEN 1 THEN e.salary * 1.1 WHEN 2 THEN e.salary * 1.05 ELSE e.salary * 1.01 END

Функции даты и времени (CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP)

ДА

SELECT e FROM app_Order e WHERE e.date = CURRENT_DATE

Функции EclipseLink (CAST, REGEXP, EXTRACT)

ДА

SELECT EXTRACT(YEAR FROM e.createTs) FROM app_MyEntity e WHERE EXTRACT(YEAR FROM e.createTs) > 2012

SELECT e FROM app_MyEntity e WHERE e.name REGEXP '.*'

SELECT CAST(e.number text) FROM app_MyEntity e WHERE e.path LIKE CAST(:ds$myEntityDs.id text)

НЕТ: CAST в запросе GROUP BY

SELECT e FROM app_Order e WHERE e.amount > 100 GROUP BY CAST(e.orderDate date)

Операторы типов сущности

ДА: тип сущности передаётся как параметр

SELECT e FROM app_Employee e WHERE TYPE(e) IN (:empType1, :empType2)

НЕТ: прямая ссылка на сущность

SELECT e FROM app_Employee e WHERE TYPE(e) IN (app_Exempt, app_Contractor)

Вызов функций

ДА: результат с операторами сравнения

SELECT u FROM sec$User u WHERE function('DAYOFMONTH', u.createTs) = 1

НЕТ: прямое использование результата функции

SELECT u FROM sec$User u WHERE function('hasRoles', u.createdBy, u.login)

IN

ДА

SELECT e FROM Employee e, IN(e.projects) p WHERE p.budget > 1000000

IS EMPTY для коллекций

ДА

SELECT e FROM Employee e WHERE e.projects IS EMPTY

KEY/VALUE

НЕТ

SELECT v.location.street, KEY(i).title, VALUE(i) FROM app_VideoStore v JOIN v.videoInventory i WHERE v.location.zipcode = '94301' AND VALUE(i) > 0

Литералы

ДА

SELECT e FROM app_Employee e WHERE e.name = 'Bob'

SELECT e FROM app_Employee e WHERE e.id = 1234

SELECT e FROM app_Employee e WHERE e.id = 1234L

SELECT s FROM app_Stat s WHERE s.ratio > 3.14F

SELECT s FROM app_Stat s WHERE s.ratio > 3.14e32D

SELECT e FROM app_Employee e WHERE e.active = TRUE

НЕТ: литералы даты и времени

SELECT e FROM app_Employee e WHERE e.startDate = {d'2012-01-03'}

SELECT e FROM app_Employee e WHERE e.startTime = {t'09:00:00'}

SELECT e FROM app_Employee e WHERE e.version = {ts'2012-01-03 09:00:00.000000001'}

MEMBER OF

ДА: для полей и запросов

SELECT d FROM app_Department d WHERE (select e from app_Employee e where e.id = :eParam) MEMBER OF e.employees

НЕТ: для литералов

SELECT e FROM app_Employee e WHERE 'write code' MEMBER OF e.codes

NEW в SELECT

ДА

SELECT NEW com.acme.example.CustomerDetails(c.id, c.status, o.count) FROM app_Customer c JOIN c.orders o WHERE o.count > 100

NULLIF/COALESCE

ДА

SELECT NULLIF(emp.salary, 10) FROM app_Employee emp

SELECT COALESCE(emp.salary, emp.salaryOld, 10) FROM app_Employee emp

NULLS FIRST, NULLS LAST в order by

ДА

SELECT h FROM sec$GroupHierarchy h ORDER BY h.level DESC NULLS FIRST

Строковые функции (CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE)

ДА

SELECT x FROM app_Magazine x WHERE CONCAT(x.title, 's') = 'JDJs'

SELECT x FROM app_Magazine x WHERE SUBSTRING(x.title, 1, 1) = 'J'

SELECT x FROM app_Magazine x WHERE LOWER(x.title) = 'd'

SELECT x FROM app_Magazine x WHERE UPPER(x.title) = 'D'

SELECT x FROM app_Magazine x WHERE LENGTH(x.title) = 10

SELECT x FROM app_Magazine x WHERE LOCATE('A', x.title, 4) = 6

SELECT x FROM app_Magazine x WHERE TRIM(TRAILING FROM x.title) = 'D'

НЕТ: TRIM не поддерживается с trim char

SELECT x FROM app_Magazine x WHERE TRIM(TRAILING 'J' FROM x.title) = 'D'

Вложенные запросы

ДА

SELECT goodCustomer FROM app_Customer goodCustomer WHERE goodCustomer.balanceOwed < (SELECT AVG(c.balanceOwed) FROM app_Customer c)

НЕТ: path-выражения вместо имени сущности в FROM подзапроса

SELECT c FROM app_Customer c WHERE (SELECT AVG(o.price) FROM c.orders o) > 100

TREAT

ДА

SELECT e FROM app_Employee e JOIN TREAT(e.projects AS app_LargeProject) p WHERE p.budget > 1000000

НЕТ: TREAT в WHERE-выражениях

SELECT e FROM Employee e JOIN e.projects p WHERE TREAT(p as LargeProject).budget > 1000000

3.4.4.4.2. Поиск подстроки без учета регистра

Для удобного формирования условия поиска без учета регистра символов и по любой части строки можно использовать префикс (?i) в значении параметра запроса. Например, имеется запрос:

select c from sales_Customer c where c.name like :name

Если в значении параметра name передать строку (?i)%doe%, то при наличии в БД записи со значением John Doe она будет найдена, несмотря на различие в регистре символа. Это произойдет потому, что ORM выполнит SQL с условием вида lower(C.NAME) like ?.

Следует иметь в виду, что при таком поиске индекс, созданный в БД по полю NAME, не используется.

3.4.4.4.3. Макросы в JPQL

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

Макросы решают следующие задачи:

  • Позволяют обойти принципиальную невозможность средствами JPQL выразить условие зависимости значения поля от текущего момента времени (не работает арифметика типа current_date-1)

  • Позволяют сравнивать с датой поля типа Timestamp (содержащие дату+время)

Рассмотрим их подробно:

@between

Имеет вид @between(field_name, moment1, moment2, time_unit) или @between(field_name, moment1, moment2, time_unit, user_timezone), где

  • field_name - имя атрибута для сравнения

  • moment1, moment2 - моменты времени, в которые должно попасть значение атрибута field_name. Каждый из моментов должен быть определен выражением с участием переменной now, к которой может быть прибавлено или отнято целое число

  • time_unit - определяет единицу измерения времени, которое прибавляется или вычитается из now в выражениях моментов, а также точность округления моментов. Может быть следующим: year, month, day, hour, minute, second.

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

Макрос преобразуется в следующее выражение JPQL: field_name >= :moment1 and field_name < :moment2

Пример 1. Покупатель создан сегодня:

select c from sales_Customer where @between(c.createTs, now, now+1, day)

Пример 2. Покупатель создан в течение последних 10 минут:

select c from sales_Customer where @between(c.createTs, now-10, now, minute)

Пример 3. Документы, датированные последними 5 днями и с учетом часового пояса текущего пользователя:

select d from sales_Doc where @between(d.createTs, now-5, now, day, user_timezone)
@today

Имеет вид @today(field_name) или @today(field_name, user_timezone) и обеспечивает формирование условия попадания значения атрибута в текущий день. По сути это частный случай макроса @between.

Пример. Пользователь создан сегодня:

select d from sales_Doc where @today(d.createTs)
@dateEquals

Имеет вид @dateEquals(field_name, parameter) или @dateEquals(field_name, parameter, user_timezone) и позволяет сформировать условие попадания значения поля field_name типа Timestamp в дату, задаваемую параметром parameter.

Пример:

select d from sales_Doc where @dateEquals(d.createTs, :param)

Текущую дату можно передать с помощью атрибута now. Для сдвига на определенное количество дней используйте now с + или -, к примеру:

select d from sales_Doc where @dateEquals(d.createTs, now-1)
@dateBefore

Имеет вид @dateBefore(field_name, parameter) или @dateBefore(field_name, parameter, user_timezone) и позволяет сформировать условие, что дата значения поля field_name типа Timestamp меньше даты, задаваемой параметром parameter.

Пример:

select d from sales_Doc where @dateBefore(d.createTs, :param)

Текущую дату можно передать с помощью атрибута now. Для сдвига на определенное количество дней используйте now с + или -, к примеру:

select d from sales_Doc where @dateBefore(d.createTs, now+1)
@dateAfter

Имеет вид @dateAfter(field_name, parameter) или @dateAfter(field_name, parameter, user_timezone) и позволяет сформировать условие, что дата значения поля field_name типа Timestamp больше или равна дате, задаваемой параметром parameter.

Пример:

select d from sales_Doc where @dateAfter(d.createTs, :param)

Текущую дату можно передать с помощью атрибута now. Для сдвига на определенное количество дней используйте now с + или -, к примеру:

select d from sales_Doc where @dateAfter(d.createTs, now-1)
@enum

Позволяет использовать полное имя константы enum вместо ее идентификатора в БД. Это упрощает поиск использований enum в коде приложения.

Пример:

select r from sec$Role where r.type = @enum(com.haulmont.cuba.security.entity.RoleType.SUPER) order by r.name
3.4.4.5. Выполнение SQL запросов

ORM позволяет выполнять SQL запросы к базе данных, возвращая как списки отдельных полей, так и экземпляры сущностей. Для этого необходимо создать объект Query или TypedQuery вызовом одного из методов EntityManager.createNativeQuery().

Если выполняется выборка отдельных колонок таблицы, то результирующий список будет содержать строки в виде Object[]. Например:

Query query = persistence.getEntityManager().createNativeQuery(
        "select ID, NAME from SALES_CUSTOMER where NAME like ?1");
query.setParameter(1, "%Company%");
List list = query.getResultList();
for (Iterator it = list.iterator(); it.hasNext(); ) {
    Object[] row = (Object[]) it.next();
    UUID id = (UUID) row[0];
    String name = (String) row[1];
}

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

Query query = persistence.getEntityManager().createNativeQuery(
        "select count(*) from SEC_USER where login = #login");
query.setParameter("login", "admin");
long count = (long) query.getSingleResult();

Если вместе с текстом запроса передан класс результирующей сущности, то возвращается TypedQuery и после выполнения производится попытка отображения результатов запроса на атрибуты сущности. Например:

TypedQuery<Customer> query = em.createNativeQuery(
    "select * from SALES_CUSTOMER where NAME like ?1",
    Customer.class);
query.setParameter(1, "%Company%");
List<Customer> list = query.getResultList();

Следует иметь в виду, что при использовании SQL колонки, соответствующие атрибутам сущностей типа UUID, возвращаются в виде UUID или в виде String, в зависимости от используемой СУБД:

  • HSQLDB - String

  • PostgreSQL - UUID

  • Microsoft SQL Server - String

  • Oracle - String

  • MySQLString

Параметры этого типа также должны задаваться либо как UUID, либо своим строковым представлением, в зависимости от используемой СУБД. Для обеспечения независимости кода от используемой СУБД рекомендуется использовать DbTypeConverter, который обеспечивает конвертацию данных между объектами Java и параметрами и результатами JDBC.

В SQL запросах можно использовать позиционные или именованные параметры. Позиционные параметры обозначаются ? с последующим номером параметра начиная с 1. Именованные параметры обозначаются знаком #. См. примеры выше.

Поведение SQL запросов, возвращающих сущности, и модифицирующих запросов (update, delete), по отношению к текущему персистентному контексту аналогично описанному для JPQL запросов.

3.4.4.6. Entity Listeners

Entity Listeners предназначены для реакции на события жизненного цикла экземпляров сущностей на уровне Middleware.

Слушатель представляет собой класс, реализующий один или несколько интерфейсов пакета com.haulmont.cuba.core.listener. Слушатель будет реагировать на события типов, соответствующих реализуемым интерфейсам.

BeforeDetachEntityListener

Метод onBeforeDetach() вызывается перед отделением объекта от EntityManager при коммите транзакции.

Данный слушатель можно использовать, например, для заполнения неперсистентных атрибутов сущности перед отправкой ее на клиентский уровень.

BeforeAttachEntityListener

Метод onBeforeAttach() вызывается перед введением объекта в персистентный контекст при выполнении операции EntityManager.merge().

Данный слушатель можно использовать, например, для заполнения персистентных атрибутов сущности перед сохранением ее в базе данных.

BeforeInsertEntityListener

Метод onBeforeInsert() вызывается перед выполнением вставки записи в БД. В данном методе возможны любые операции с текущим EntityManager.

AfterInsertEntityListener

Метод onAfterInsert() вызывается после выполнения вставки записи в БД, но до коммита транзакции. В данном методе нельзя модифицировать текущий персистентный контекст, однако можно производить изменения в БД с помощью QueryRunner.

BeforeUpdateEntityListener

Метод onBeforeUpdate() вызывается перед изменением записи в БД. В данном методе возможны любые операции с текущим EntityManager.

AfterUpdateEntityListener

Метод onAfterUpdate() вызывается после изменения записи в БД, но до коммита транзакции. В данном методе нельзя модифицировать текущий персистентный контекст, однако можно производить изменения в БД с помощью QueryRunner.

BeforeDeleteEntityListener

Метод onBeforeDelete() вызывается перед удалением записи из БД (или в случае мягкого удаления - перед изменением записи). В данном методе возможны любые операции с текущим EntityManager.

AfterDeleteEntityListener

Метод onAfterDelete() вызывается после удаления записи из БД (или в случае мягкого удаления - после изменения записи), но до коммита транзакции. В данном методе нельзя модифицировать текущий персистентный контекст, однако можно производить изменения в БД с помощью QueryRunner.

Entity Listener должен являться Spring-бином, поэтому в нем можно использовать инжекцию в поля и сеттеры. Для всех экземпляров некоторого класса сущности создается один экземпляр слушателя определенного типа, поэтому слушатель не должен иметь изменяемого состояния.

Следует иметь в виду, что для BeforeInsertEntityListener фреймворк гарантирует managed state только для корневой сущности, принимаемой слушателем. Ее ссылки, встречающиеся в графе объектов, могут быть в состоянии detached. Поэтому если вам необходимо изменять эти объекты, используйте метод EntityManager.merge(), а если только иметь возможность обращаться к любым их атрибутам, то метод EntityManager.find(). Например:

package com.company.sample.listener;

import com.company.sample.core.DiscountCalculator;
import com.company.sample.entity.*;
import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.listener.*;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
import java.math.BigDecimal;

@Component("sample_OrderEntityListener")
public class OrderEntityListener implements
        BeforeInsertEntityListener<Order>,
        BeforeUpdateEntityListener<Order>,
        BeforeDeleteEntityListener<Order> {

    @Inject
    private DiscountCalculator discountCalculator; // a managed bean of the middle tier

    @Override
    public void onBeforeInsert(Order entity, EntityManager entityManager) {
        calculateDiscount(entity.getCustomer(), entityManager);
    }

    @Override
    public void onBeforeUpdate(Order entity, EntityManager entityManager) {
        calculateDiscount(entity.getCustomer(), entityManager);
    }

    @Override
    public void onBeforeDelete(Order entity, EntityManager entityManager) {
        calculateDiscount(entity.getCustomer(), entityManager);
    }

    private void calculateDiscount(Customer customer, EntityManager entityManager) {
        if (customer == null)
            return;

        // Delegate calculation to a managed bean of the middle tier
        BigDecimal discount = discountCalculator.calculateDiscount(customer.getId());

        // Merge customer instance because it comes to onBeforeInsert as part of another
        // entity's object graph and can be detached
        Customer managedCustomer = entityManager.merge(customer);

        // Set the discount for the customer. It will be saved on transaction commit.
        managedCustomer.setDiscount(discount);
    }
}

Все слушатели кроме BeforeAttachEntityListener выполняются внутри транзакции. Это значит, что если внутри слушателя выбрасывается исключение, текущая транзакция откатывается и все изменения в базе данных теряются.

Если вам необходимо выполнить некоторые действия после успешного завершения транзакции, используйте callback-интерфейс TransactionSynchronization фреймворка Spring для того чтобы отложить выполнение до нужной фазы транзакции. Например:

package com.company.sales.service;

import com.company.sales.entity.Customer;
import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.listener.BeforeInsertEntityListener;
import com.haulmont.cuba.core.listener.BeforeUpdateEntityListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;

@Component("sales_CustomerEntityListener")
public class CustomerEntityListener implements BeforeInsertEntityListener<Customer>, BeforeUpdateEntityListener<Customer> {

    @Override
    public void onBeforeInsert(Customer entity, EntityManager entityManager) {
        printCustomer(entity);
    }

    @Override
    public void onBeforeUpdate(Customer entity, EntityManager entityManager) {
        printCustomer(entity);
    }

    private void printCustomer(Customer customer) {
        System.out.println("In transaction: " + customer);

        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                System.out.println("After transaction commit: " + customer);
            }
        });
    }
}
Регистрация entity listeners

Entity Listener может быть задан двумя способами:

  • Статически - имена бинов слушателей указываются в аннотации @Listeners на классе сущности:

    @Entity(...)
    @Table(...)
    @Listeners("sample_MyEntityListener")
    public class MyEntity extends StandardEntity {
        ...
    }
  • Динамически - имя бина слушателя передается в метод addListener() бина EntityListenerManager. Таким способом можно добавить слушатель для сущности, находящейся в компоненте приложения. В примере ниже слушатель, реализованный бином sample_UserEntityListener, добавляется сущности User, которая определена во фреймворке:

    package com.company.sample.core;
    
    import com.haulmont.cuba.core.global.Events;
    import com.haulmont.cuba.core.sys.events.AppContextInitializedEvent;
    import com.haulmont.cuba.core.sys.listener.EntityListenerManager;
    import com.haulmont.cuba.security.entity.User;
    import org.springframework.context.event.EventListener;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;
    import javax.inject.Inject;
    
    @Component("sample_AppLifecycle")
    public class AppLifecycle {
    
        @Inject
        private EntityListenerManager entityListenerManager;
    
        @EventListener(AppContextInitializedEvent.class) // notify after AppContext is initialized
        @Order(Events.LOWEST_PLATFORM_PRECEDENCE + 100)  // run after all framework listeners
        public void initEntityListeners() {
            entityListenerManager.addListener(User.class, "sample_UserEntityListener");
        }
    }

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

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

  2. После всех предков вызываются динамически добавленные слушатели данного класса, затем статически назначенные.

3.4.5. Управление транзакциями

В данном разделе рассмотрены различные аспекты управления транзакциями в CUBA-приложениях.

3.4.5.1. Программное управление транзакциями

Программное управление транзакциями осуществляется с помощью интерфейса com.haulmont.cuba.core.Transaction, ссылку на который можно получить методами createTransaction() или getTransaction() интерфейса инфраструктуры Persistence.

Метод createTransaction() создает новую транзакцию и возвращает интерфейс Transaction. Последующие вызовы методов commit(), commitRetaining(), end() этого интерфейса управляют созданной транзакцией. Если в момент создания существовала другая транзакция, то она будет приостановлена, и возобновлена после завершения созданной.

Метод getTransaction() вызывает либо создание новой, либо присоединение к текущей транзакции. Если в момент вызова существовала активная транзакция, то метод успешно завершается, и последующие вызовы commit(), commitRetaining(), end() не оказывают никакого влияния на существующую транзакцию. Однако если end() вызван без предварительного вызова commit(), то текущая транзакция помечается как RollbackOnly.

Примеры программного управления транзакцией:

@Inject
private Metadata metadata;
@Inject
private Persistence persistence;
...
// try-with-resources style
try (Transaction tx = persistence.createTransaction()) {
    Customer customer = metadata.create(Customer.class);
    customer.setName("John Smith");
    persistence.getEntityManager().persist(customer);
    tx.commit();
}
// plain style
Transaction tx = persistence.createTransaction();
try {
    Customer customer = metadata.create(Customer.class);
    customer.setName("John Smith");
    persistence.getEntityManager().persist(customer);
    tx.commit();
} finally {
    tx.end();
}

Интерфейс Transaction имеет также метод execute(), принимающий на вход класс-действие или lambda-выражение, которое нужно выполнить в данной транзакции. Это позволяет организовать управление транзакциями в функциональном стиле, например:

UUID customerId = persistence.createTransaction().execute((EntityManager em) -> {
    Customer customer = metadata.create(Customer.class);
    customer.setName("ABC");
    em.persist(customer);
    return customer.getId();
});

Customer customer = persistence.createTransaction().execute(em ->
        em.find(Customer.class, customerId, "_local"));

Следует иметь в виду, что метод execute() у некоторого экземпляра Transaction можно вызвать только один раз, так как после выполнения кода действия транзакция завершается.

3.4.5.2. Декларативное управление транзакциями

Любой метод Spring-бина Middleware можно пометить аннотацией @org.springframework.transaction.annotation.Transactional, что вызовет автоматическое создание транзакции при вызове этого метода. В таком методе не нужно вызывать Persistence.createTransaction(), а можно сразу получать EntityManager и работать с ним.

Для аннотации @Transactional можно указать параметры, в том числе:

  • propagation - режим создания транзакции. Значение REQUIRED соответствует getTransaction(), значение REQUIRES_NEW - createTransaction(). По умолчанию REQUIRED.

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void doSomething() {
    }
  • value - имя хранилища данных. Если опущено, подразумевается основная БД. Например:

    @Transactional("db1")
    public void doSomething() {
    }

Декларативное управление транзакциями позволяет уменьшить количество boilerplate кода, однако имеет следующий недостаток: коммит транзакции происходит вне прикладного кода, что часто затрудняет отладку, т.к. скрывается момент отправки изменений в БД и перехода сущностей в состояние Detached. Кроме того, следует иметь в виду, что декларативная разметка сработает только в случае вызова метода контейнером, т.е. вызов транзакционного метода из другого метода того же самого объекта не приведет к старту транзакции.

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

3.4.5.3. Примеры взаимодействия транзакций
3.4.5.3.1. Откат вложенной транзакции

Если вложенная транзакция создана через getTransaction(), то ее откат приведет к невозможности коммита охватывающей транзакции. Например:

void methodA() {
    Transaction tx = persistence.createTransaction();
    try {
        methodB(); (1)
        tx.commit(); (4)
    } finally {
        tx.end();
    }
}

void methodB() {
    Transaction tx = persistence.getTransaction();
    try {
        tx.commit(); (2)
    } catch (Exception e) {
        return; (3)
    } finally {
        tx.end();
    }
}
1 вызываем метод, создающий вложенную транзакцию
2 предположим, что здесь выбрасывается исключение
3 обрабатываем исключение и выходим
4 здесь будет выброшено исключение, так как транзакция помечена как rollback only

Если же транзакция в methodB() будет создана через createTransaction(), то ее откат не окажет никакого влияния на коммит охватывающей транзакции в methodA().

3.4.5.3.2. Чтение и изменение данных во вложенной транзакции

Рассмотрим сначала зависимую вложенную транзакцию, создаваемую через getTransaction():

void methodA() {
    Transaction tx = persistence.createTransaction();
    try {
        EntityManager em = persistence.getEntityManager();

        Employee employee = em.find(Employee.class, id); (1)
        assertEquals("old name", employee.getName());

        employee.setName("name A"); (2)

        methodB(); (3)

        tx.commit(); (8)
    } finally {
      tx.end();
    }
}

void methodB() {
    Transaction tx = persistence.getTransaction();
    try {
        EntityManager em = persistence.getEntityManager(); (4)

        Employee employee = em.find(Employee.class, id); (5)

        assertEquals("name A", employee.getName()); (6)
        employee.setName("name B");

        tx.commit(); (7)
    } finally {
      tx.end();
    }
}
1 загружаем сущность, где name == "old name"
2 указываем новое значение для поля
3 вызываем метод, создающий вложенную транзакцию
4 получаем тот же экземпляр EntityManager, что и в methodA
5 загружаем сущность с тем же идентификатором
6 значение поля новое, так как мы работаем в том же persistent context, и запросов к БД не было
7 commit в этот момент не происходит
8 изменения сохраняются в БД, в них будет содержаться "name B"

Теперь рассмотрим тот же самый пример с независимой вложенной транзакцией, создаваемой через createTransaction():

void methodA() {
    Transaction tx = persistence.createTransaction();
    try {
        EntityManager em = persistence.getEntityManager();

        Employee employee = em.find(Employee.class, id); (1)
        assertEquals("old name", employee.getName());

        employee.setName("name A"); (2)

        methodB(); (3)

        tx.commit(); (8)
    } finally {
      tx.end();
    }
}

void methodB() {
    Transaction tx = persistence.createTransaction();
    try {
        EntityManager em = persistence.getEntityManager(); (4)

        Employee employee = em.find(Employee.class, id); (5)

        assertEquals("old name", employee.getName()); (6)

        employee.setName("name B"); (7)

        tx.commit();
    } finally {
      tx.end();
    }
}
1 загружаем сущность, где name == "old name"
2 указываем новое значение для поля
3 вызываем метод, создающий вложенную транзакцию
4 создаём новый экземпляр EntityManager, т.к. это новая транзакция
5 загружаем сущность с тем же идентификатором
6 значение поля старое, так как из БД загружен старый экземпляр сущности
7 изменения сохраняются в БД, значение "name B" теперь будет храниться в БД
8 из-за оптимистичной блокировки выбрасывается исключение, commit не выполняется

В последнем случае исключение в точке (8) возникнет, только если сущность является оптимистично блокируемой, т.е. если она реализует интерфейс Versioned.

3.4.5.4. Параметры транзакций
Таймаут транзакции

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

При программном управлении транзакциями таймаут включается путем передачи объекта TransactionParams в метод Persistence.createTransaction(). Например:

Transaction tx = persistence.createTransaction(new TransactionParams().setTimeout(2));

При декларативном управлении транзакциями используется параметр timeout аннотации @Transactional, например:

@Transactional(timeout = 2)
public void someServiceMethod() {
...

Таймаут по умолчанию может быть задан в свойстве приложения cuba.defaultQueryTimeoutSec.

Read-only транзации

Транзакцию можно пометить как read-only если она предназначена только для чтения данных из БД. Например, все методы load в DataManager используют read-only транзакции по умолчанию. Read-only транзакции улучшают производительность системы, потому что платформа не выполняет код, обрабатывающий возможные изменения в сущностях. Кроме того, не вызываются BeforeCommit transaction listeners.

Если персистентный контекст read-only транзакции содержит измененные сущности, то при попытке коммита транзакции будет выброшено исключение IllegalStateException. Это означает, что помечать транзакцию как read-only следует только если вы уверены, что она не модифицирует никакие сущности.

При программном управлении транзакциями признак read-only включается путем передачи объекта TransactionParams в метод Persistence.createTransaction(). Например:

Transaction tx = persistence.createTransaction(new TransactionParams().setReadOnly(true));

При декларативном управлении транзакциями используется параметр readOnly аннотации @Transactional, например:

@Transactional(readOnly = true)
public void someServiceMethod() {
...
3.4.5.5. Transaction Listeners

Transaction listeners предназначены для реакции на события жизненного цикла транзакций. В отличие от entity listeners, они не привязаны к типу сущности и вызываются для каждой транзакции.

Слушатель должен быть Spring-бином, реализующим один или оба интерфейса BeforeCommitTransactionListener и AfterCompleteTransactionListener.

BeforeCommitTransactionListener

Метод beforeCommit() вызывается перед коммитом транзакции после всех entity listeners если транзакция не является read-only. Метод принимает текущий EntityManager и коллекцию сущностей текущего персистентного контекста.

Данный слушатель можно использовать для обеспечения сложных бизнес-правил, вовлекающих различные сущности. В примере ниже атрибут amount сущности Order должен рассчитываться на основе значения discount, находящегося в заказе, и атрибутов price и quantity экземпляров сущности OrderLine, составляющих заказ.

@Component("demo_OrdersTransactionListener")
public class OrdersTransactionListener implements BeforeCommitTransactionListener {

    @Inject
    private PersistenceTools persistenceTools;

    @Override
    public void beforeCommit(EntityManager entityManager, Collection<Entity> managedEntities) {
        // gather all orders affected by changes in the current transaction
        Set<Order> affectedOrders = new HashSet<>();

        for (Entity entity : managedEntities) {
            // skip not modified entities
            if (!persistenceTools.isDirty(entity))
                continue;

            if (entity instanceof Order)
                affectedOrders.add((Order) entity);
            else if (entity instanceof OrderLine) {
                Order order = ((OrderLine) entity).getOrder();
                // a reference can be detached, so merge it into current persistence context
                affectedOrders.add(entityManager.merge(order));
            }
        }
        // calculate amount for each affected order by its lines and discount
        for (Order order : affectedOrders) {
            BigDecimal amount = BigDecimal.ZERO;
            for (OrderLine orderLine : order.getOrderLines()) {
                if (!orderLine.isDeleted()) {
                    amount = amount.add(orderLine.getPrice().multiply(orderLine.getQuantity()));
                }
            }
            BigDecimal discount = order.getDiscount().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_DOWN);
            order.setAmount(amount.subtract(amount.multiply(discount)));
        }
    }
}
AfterCompleteTransactionListener

Метод afterComplete() вызывается после завершения транзакции. Метод принимает параметр, указывающий, была ли транзакция успешно закоммичена, и коллекцию detached сущностей, содержавшихся в персистентном контексте завершенной транзакции.

Пример использования:

@Component("demo_OrdersTransactionListener")
public class OrdersTransactionListener implements AfterCompleteTransactionListener {

    private Logger log = LoggerFactory.getLogger(OrdersTransactionListener.class);

    @Override
    public void afterComplete(boolean committed, Collection<Entity> detachedEntities) {
        if (!committed)
            return;

        for (Entity entity : detachedEntities) {
            if (entity instanceof Order) {
                log.info("Order: " + entity);
            }
        }
    }
}

3.4.6. Кэши сущностей и запросов

Кэш сущностей (Entity Cache)

Кэш сущностей предоставляется ORM фреймворком EclipseLink. Он хранит в памяти недавно прочитанные или записанные экземпляры сущностей, тем самым сокращая доступ к базе данных и увеличивая производительность.

Кэш сущностей используется только при извлечении сущностей по идентификатору, поэтому запросы по другим атрибутам по-прежнему выполняются на базе данных. Тем не менее, эти запросы могут стать проще и быстрее, если связанные сущности находятся в кэше. Например, если вы запрашиваете Заказы вместе со связанными Заказчиками, и не используете кэш, то SQL-запрос будет содержать JOIN с таблицей заказчиков. Если же сущность Заказчик закэширована, SQL-запрос будет только по таблице заказов, а связанные заказчики будут извлечены из кэша.

Для того, чтобы включить кэш сущностей, установите следующие свойства приложения в файле app.properties модуля core вашего проекта:

  • eclipselink.cache.shared.sales_Customer = true - включает кэширование сущности sales_Customer.

  • eclipselink.cache.size.sales_Customer = 500 - устанавливает размер кэша для сущности sales_Customer в 500 экземпляров.

    Если кэширование включено, всегда рекомендуется увеличить размер кэша (по умолчанию - 100). В противном случае, если запрос вернёт более 100 записей, то каждая запись будет извлечена отдельной операцией.

Факт кэширования сущности влияет на то, какой fetch mode выбирается платформой при загрузке графов сущностей. Если некоторый ссылочный атрибут представляет собой кэшируемую сущность, то fetch mode всегда будет UNDEFINED, что позволяет ORM извлекать ссылку из кэша вместо добавления в запрос JOIN или выполнения отдельного batch-запроса.

Платформа обеспечивает координацию кэша сущностей в кластере middleware. Когда кэшированный экземпляр сущности обновляется или удаляется на одном узле кластера, тот же экземпляр на других узлах (если он загружен) будет инвалидирован, что приведет к загрузке свежего состояния из БД при следующей операции с данным экземпляром.

Кэш запросов (Query Cache)

Кэш запросов сохраняет идентификаторы экземпляров сущностей, возвращаемых JPQL-запросами, тем самым естественно дополняя кэш сущностей.

Например, если для сущности sales_Customer разрешен entity cache, и запрос select c from sales_Customer c where c.grade = :grade выполняется первый раз, происходит следующее:

  • ORM выполняет запрос в базе данных.

  • Загруженные экземпляры сущности Customer помещаются в entity cache.

  • В кэш запросов помещается соответствие между текстом запроса вместе с параметрами и списком идентификаторов загруженных экземпляров.

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

Запросы по умолчанию не кэшируются. Указать, что запрос должен кэшироваться, можно на различных уровнях приложения:

  • Методом setCacheable() интерфейса Query при работе с EntityManager.

  • Методом setCacheable() интерфейса LoadContext.Query при работе с DataManager.

  • Методом setCacheable() интерфейса CollectionLoader или в XML-атрибуте cacheable при работе с загрузчиками данных.

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

Кэш запросов автоматически инвалидируется, когда через ORM выполняются операции создания, изменения или удаления с сущностями соответствующего типа. Инвалидация работает по всему кластеру среднего слоя.

JMX-бин app-core.cuba:type=QueryCacheSupport можно использовать для мониторинга состояния кэша и для удаления запросов из кэша. Например, если вы изменили некоторый экземпляр сущности sales_Customer напрямую в БД, необходимо удалить все закэшированные запросы по этой сущности с помощью операции evict() с аргументом sales_Customer.

На поведение кэша запросов оказывают влияние следующие свойства приложения:

3.4.7. EntityChangedEvent

Руководство Decouple Business Logic with Application Events демонстрирует использование EntityChangedEvent.

EntityChangedEvent - это ApplicationEvent, который посылается фреймворком на среднем слое, когда некоторый экземпляр сущности сохраняется в базу данных. Данное событие может быть обработано как внутри текущей транзакции, так и после ее завершения (при использовании @TransactionalEventListener).

Событие посылается только если на сущности есть аннотация @PublishEntityChangedEvents. Не забудьте добавить эту аннотацию классам сущностей, для которых вы хотите обрабатывать EntityChangedEvent.

Объект EntityChangedEvent содержит не сам измененный экземпляр сущности, а только его id. Кроме того, метод getOldValue(attributeName) возвращает идентификаторы ссылок вместо самих объектов. Поэтому при необходимости, разработчик должен загрузить требуемые сущности с указанием требуемого представления и других параметров.

Ниже приведен пример обработки EntityChangedEvent для сущности Customer в текущей транзакции и после ее завершения:

package com.company.demo.core;

import com.company.demo.entity.Customer;
import com.haulmont.cuba.core.app.events.AttributeChanges;
import com.haulmont.cuba.core.app.events.EntityChangedEvent;
import com.haulmont.cuba.core.entity.contracts.Id;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import java.util.UUID;

@Component("demo_CustomerChangedListener")
public class CustomerChangedListener {

    @TransactionalEventListener(
            phase = TransactionPhase.BEFORE_COMMIT (1)
    )
    public void beforeCommit(EntityChangedEvent<Customer, UUID> event) {
        Id<Customer, UUID> entityId = event.getEntityId(); (2)
        EntityChangedEvent.Type changeType = event.getType(); (3)

        AttributeChanges changes = event.getChanges();
        if (changes.isChanged("name")) { (4)
            String oldName = changes.getOldValue("name"); (5)
            // ...
        }
    }

    @TransactionalEventListener(
            phase = TransactionPhase.AFTER_COMMIT (6)
    )
    public void afterCommit(EntityChangedEvent<Customer, UUID> event) {
        (7)
    }
}
1 - данный обработчик вызывается внутри текущей транзакции.
2 - id измененной сущности.
3 - тип изменения: CREATED, UPDATED or DELETED.
4 - можно проверить, изменился ли определенный атрибут.
5 - можно получить старое значение измененного атрибута.
6 - данный обработчик вызывается после коммита транзакции.
7 - после коммита событие содержит те же значения что и внутри транзакции.

Если обработчик вызывается внутри транзакции, ее можно откатить путем выбрасывания исключения. При этом в БД никакие изменения не сохранятся. Если вы не хотите, чтобы пользователь получил какое-либо оповещение, используйте SilentException.

Если "after commit" обработчик выбрасывает исключение, оно будет залоггировано, но не передано клиенту (т.е. пользователь не получит сообщения об ошибке в UI).

При обработке EntityChangedEvent внутри транзакции (TransactionPhase.BEFORE_COMMIT), обязательно используйте TransactionalDataManager для загрузки текущего состояния измененной сущности из базы данных. Если использовать DataManager, то будет создана новая транзакция, что может повлечь дедлок в базе данных при попытке чтения незакоммиченных данных.

В обработчике, вызываемом после коммита транзакции (TransactionPhase.AFTER_COMMIT), используйте DataManager, или создайте новую транзакцию явно перед использованием TransactionalDataManager.

Ниже приведен пример использования EntityChangedEvent для изменения связанных сущностей.

Предположим, имеются сущности Order, OrderLine и Product, как в приложении Sales, но сущность Product дополнительно имеет булевский атрибут special, а у сущности Order есть атрибут numberOfSpecialProducts, который должен быть пересчитан при создании и удалении экземпляров OrderLine в составе Order.

Создадим следующий класс с методом, аннотированным @EventListener, который будет вызываться при изменении сущностей OrderLine перед коммитом транзакции:

package com.company.sales.listener;

import com.company.sales.entity.Order;
import com.company.sales.entity.OrderLine;
import com.haulmont.cuba.core.TransactionalDataManager;
import com.haulmont.cuba.core.app.events.EntityChangedEvent;
import com.haulmont.cuba.core.entity.contracts.Id;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import javax.inject.Inject;
import java.util.UUID;

@Component("sales_OrderLineChangedListener")
public class OrderLineChangedListener {

    @Inject
    private TransactionalDataManager txDm;

    @TransactionalEventListener(
            phase = TransactionPhase.BEFORE_COMMIT
    )
    public void beforeCommit(EntityChangedEvent<OrderLine, UUID> event) {
        Order order;
        if (event.getType() != EntityChangedEvent.Type.DELETED) { (1)
            order = txDm.load(event.getEntityId()) (2)
                    .view("orderLine-with-order") (3)
                    .one()
                    .getOrder(); (4)
        } else {
            Id<Order, UUID> orderId = event.getChanges().getOldReferenceId("order"); (5)
            order = txDm.load(orderId).one();
        }

        long count = txDm.load(OrderLine.class) (6)
                .query("select o from sales_OrderLine o where o.order = :order")
                .parameter("order", order)
                .view("orderLine-with-product")
                .list().stream()
                .filter(orderLine -> Boolean.TRUE.equals(orderLine.getProduct().getSpecial()))
                .count();

        order.setNumberOfSpecialProducts((int) count);

        txDm.save(order); (7)
    }
}
1 - если экземпляр OrderLine не удален, можно загрузить его из БД по идентификатору.
2 - метод event.getEntityId() возвращает id измененного экземпляра OrderLine.
3 - используем представление, которое содержит OrderLine вместе с Order, которому он принадлежит. Представление должно содержать атрибут Order.numberOfSpecialProducts, так как его необходимо будет обновить.
4 - получаем Order из загруженного OrderLine.
5 - если экземпляр OrderLine был только что удален, его нельзя загрузить из БД, но метод event.getChanges() возвращает все атрибуты удаленной сущности, включая идентификаторы связанных сущностей. Поэтому можно загрузить связанный Order по его id.
6 - загружаем все экземпляры OrderLine для данного Order, отфильтровываем по Product.special и считаем их. Представление должно содержать OrderLine вместе со связанным Product.
7 - сохраняем Order после изменения его атрибута.

3.4.8. EntityPersistingEvent

EntityPersistingEvent - это ApplicationEvent, который посылается фреймворком на среднем слое, перед тем как новый экземпляр сущности сохраняется в базу данных. В момент отсылки данного события существует открытая транзакция.

EntityPersistingEvent можно использовать для инициализации атрибутов сущности перед ее созданием в БД:

package com.company.demo.core;

import com.company.demo.entity.Customer;
import com.haulmont.cuba.core.app.events.EntityPersistingEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component("demo_CustomerChangedListener")
public class CustomerChangedListener {

    @EventListener
    void beforePersist(EntityPersistingEvent<Customer> event) {
        Customer customer = event.getEntity();
        customer.setCode(obtainNewCustomerCode(customer));
    }

    // ...
}

3.4.9. Системная аутентификация

При выполнении пользовательских запросов программному коду Middleware через интерфейс UserSessionSource всегда доступна информация о текущем пользователе. Это возможно потому, что при получении запроса с клиентского уровня в потоке выполнения автоматически устанавливается соответствующий объект SecurityContext.

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

Такого рода аутентификация называется системной, так как не требует участия пользователя - средний слой приложения просто создает (или использует имеющуюся) пользовательскую сессию, и устанавливает в потоке выполнения соответствующий объект SecurityContext.

Обеспечить системную аутентификацию некоторого участка кода можно следующими способами:

  • явно используя бин com.haulmont.cuba.security.app.Authentication, например:

    @Inject
    protected Authentication authentication;
    ...
    authentication.begin();
    try {
        // authenticated code
    } finally {
        authentication.end();
    }
  • добавив методу бина аннотацию @Authenticated, например:

    @Authenticated
    public String foo(String value) {
        // authenticated code
    }

Во втором случае также используется бин Authentication, но неявно, через интерцептор AuthenticationInterceptor, который перехватывает вызовы всех методов бинов с аннотацией @Authenticated.

В приведенных примерах пользовательская сессия будет создаваться от лица пользователя, логин которого указан в свойстве приложения cuba.jmxUserLogin. Если требуется аутентификация от имени другого пользователя, нужно воспользоваться первым вариантом и передать в метод begin() логин нужного пользователя.

Если в момент выполнения Authentication.begin() в текущем потоке выполнения присутствует активная пользовательская сессия, то она не заменяется - соответственно, код, требующий аутентификации, будет выполняться с имеющейся сессией, и последующий метод end() не будет очищать поток.

Например, вызов метода JMX-бина из встроенной в Web Client консоли JMX, если бин находится в той же JVM, что и блок WebClient, к которому в данный момент подключен пользователь, будет выполнен от имени текущего зарегистрированного в системе пользователя, независимо от наличия системной аутентификации.

3.5. Универсальный пользовательский интерфейс

Подсистема универсального пользовательского интерфейса (Generic UI, GUI) позволяет разрабатывать экраны UI, используя Java и XML. Использование XML не обязательно, но позволяет описывать компоновку экрана декларативно и снижает объем кода, требуемого для создания пользовательского интерфейса.

ClientStructure
Рисунок 9. Структура универсального пользовательского интерфейса

Экраны приложения состоят из следующих частей:

  • Дескрипторы - XML-файлы, содержащие информацию о компоновке экрана и компонентах данных.

  • Контроллеры - классы Java, содержащие логику инициализации и обработки событий от экрана и его компонентов.

Код экранов приложения взаимодействует с интерфейсами визуальных компонентов (VCL Interfaces). Эти интерфейсы реализованы с использованием компонентов фреймворка Vaadin.

Библиотека визуальных компонентов (Visual Components Library, VCL) содержит большой набор готовых компонентов.

Компоненты данных (Data components) предоставляют унифицированный интерфейс для связывания визуальных компонентов с сущностями модели данных и для работы с сущностями в контроллерах экранов.

Инфраструктура (Infrastructure) включает в себя главное окно приложения и другие общие клиентские механизмы.

3.5.1. Экраны

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

Основу экрана составляет Java- или Groovy-класс, который называется контроллером. Компоновка экрана обычно определяется в XML-файле, именуемом дескриптором экрана.

Чтобы отобразить экран, платформа создаёт новый экземпляр визуального компонента Window, подключает его к контроллеру экрана и загружает компоненты, входящие в компоновку экрана, в качестве дочерних компонентов окна. Затем окно этого экрана добавляется к главному экрану приложения.

Фрагмент - это еще один структурный элемент UI, который можно использовать в качестве компонента экранов или других фрагментов. Внутреннее устройство фрагмента аналогично экрану, но фрагмент имеет специфический жизненный цикл, а в корне иерархии визуальных компонентов находится Fragment вместо Window. Фрагменты, также как экраны, состоят из контроллеров и XML-дескрипторов.

3.5.1.1. Контроллер экрана

Контроллер экрана - это Java или Groovy класс, который содержит в себе логику инициализации экрана и обработки событий. Чаще всего контроллер связан с XML-дескриптором экрана, который декларативно описывает компоновку экрана и контейнеры данных, однако и в контроллере можно программно создавать визуальные и невизуальные компоненты.

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

  • Window - самостоятельное окно, которое можно отобразить внутри главного окна приложения во вкладке или в виде модального диалогового окна.

  • Fragment - легковесный компонент, который можно добавлять к окнам или к другим фрагментам.

Контроллеры также подразделяются на две отдельные категории по типам используемых фреймвов:

  • Screen - базовый класс контроллеров окон.

  • ScreenFragment - базовый класс контроллеров фрагментов.

screens
Рисунок 10. Контроллеры и фреймы

Класс Screen предоставляет базовую функциональность для любого типа самостоятельных экранов. Для экранов, предназначенных для работы с сущностями, существуют отдельные, более специфичные классы:

  • StandardEditor - базовый класс контроллеров экранов редактирования.

  • StandardLookup - базовый класс контроллеров экранов просмотра и выбора.

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

controller base classes
Рисунок 11. Базовые классы контроллеров
3.5.1.1.1. Аннотации контроллеров

Аннотации на классе контроллера используются для предоставления фреймворку информации об экране. Некоторые аннотации применимы для любого типа экрана, некоторые должны быть использованы только в экранах редактирования или поиска.

Следующий пример демонстрирует использование общих аннотаций:

package com.company.demo.web.screens;

import com.haulmont.cuba.gui.screen.*;

@UiController("demo_FooScreen")
@UiDescriptor("foo-screen.xml")
@LoadDataBeforeShow
@MultipleOpen
@DialogMode(forceDialog = true)
public class FooScreen extends Screen {
}
  • @UiController - указывает, что данный класс является контроллером экрана. Значение аннотации задает id экрана, который используется для ссылки на экран из главного меню или при открытии экрана программно.

  • @UiDescriptor - соединяет контроллер с XML-дескриптором. Значение аннотации указывает путь к файлу. Если значение содержит только имя файла, подразумевается что файл находится в том же пакете что и класс контроллера.

  • @LoadDataBeforeShow - указывает, что все загрузчики данных должны быть запущены автоматически перед показом экрана. Точнее, данные загружаются после вызова всех обработчиков BeforeShowEvent, но до обработчиков AfterShowEvent. Если вам необходимо выполнить какие-либо действия при загрузке данных перед показом экрана, удалите данную аннотацию или установите ее значение в false и используйте метод getScreenData().loadAll() или методы load() нужных загрузчиков в слушателе события BeforeShowEvent. Рассмотрите также использование фасета DataLoadCoordinator для декларативного управления загрузкой данных.

  • @MultipleOpen - указывает, что из главного меню можно открыть несколько экземпляров данного экрана. По умолчанию, когда пользователь выбирает пункт главного меню, фреймворк проверяет, не открыт ли уже наверху какой-либо вкладки главного окна экран с тем же классом и id. Если такой экран найден, он закрывается, и новый экран открывается в новой вкладке. Когда на экране присутствует аннотация @MultipleOpen, никаких проверок не производится, и новый экземпляр экрана просто открывается в новой вкладке.

    Можно предоставить собственный способ проверки, является ли экран тем же самым, если переопределить метод isSameScreen() контроллера.

  • @DialogMode - позволяет указать параметры геометрии и поведения экрана при открытии его в диалоговом окне. Данная аннотация соответствует элементу <dialogMode> дексриптора экрана и может быть использована вместо него. Значения из XML имеют более высокий приоритет для всех параметров кроме forceDialog. Значение параметра forceDialog объединяется: если оно установлено в true или в XML, или в аннотации, то экран всегда открывается в диалоге.

Пример аннотаций, специфичных для экранов выбора:

package com.company.demo.web.screens;

import com.haulmont.cuba.gui.screen.*;
import com.company.demo.entity.Customer;

// common annotations
@UiController("demo_Customer.browse")
@UiDescriptor("customer-browse.xml")
@LoadDataBeforeShow
// lookup-specific annotations
@LookupComponent("customersTable")
@PrimaryLookupScreen(Customer.class)
public class CustomerBrowse extends StandardLookup<Customer> {
}
  • @LookupComponent - указывает id UI-компонента, который должен быть использован для получения значения из экрана выбора.

    Вместо использования данной аннотации, можно указать компонент выбора программно, если переопределить метод контроллера getLookupComponent().

  • @PrimaryLookupScreen - указывает, что данный экран является экраном выбора по умолчанию для сущностей заданного типа. Данная аннотация имеет больший приоритет чем конвенция {entity_name}.lookup / {entity_name}.browse.

Пример аннотаций, специфичных для экранов редактирования:

package com.company.demo.web.data.sort;

import com.haulmont.cuba.gui.screen.*;
import com.company.demo.entity.Customer;

// common annotations
@UiController("demo_Customer.edit")
@UiDescriptor("customer-edit.xml")
@LoadDataBeforeShow
// editor-specific annotations
@EditedEntityContainer("customerDc")
@PrimaryEditorScreen(Customer.class)
public class CustomerEdit extends StandardEditor<Customer> {
}
  • @EditedEntityContainer - указывает контейнер данных, содержащий редактируемую сущность.

    Вместо использования данной аннотации, можно указать контейнер программно, если переопределить метод контроллера getEditedEntityContainer()

  • @PrimaryEditorScreen - указывает, что данный экран является экраном редактирования по умолчанию для сущностей заданного типа. Данная аннотация имеет больший приоритет чем конвенция {entity_name}.edit.

3.5.1.1.2. Методы контроллеров

В данном разделе описаны некоторые методы базовых классов контроллеров экранов, которые можно вызывать или переопределять в коде приложения.

Общие для всех экранов методы
  • show() - отображает экран. Данный метод обычно вызывается после создания экрана, как описано в разделе Открытие экранов.

  • close() - закрывает экран с переданным значением StandardOutcome или объектом CloseAction. Например:

    @Subscribe("closeBtn")
    public void onCloseBtnClick(Button.ClickEvent event) {
        close(StandardOutcome.CLOSE);
    }

    Значение параметра доступно в BeforeCloseEvent и AfterCloseEvent, поэтому информация о причине закрытия экрана может быть получена в слушателях этих событий. Подробная информация об использовании этих слушателей приведена в разделе Выполнение кода после закрытия и возврат значений.

  • getScreenData() - возвращает объект ScreenData, в котором зарегистрированы все компоненты данных, объявленные в XML-дескрипторе экрана. Метод loadAll() этого объекта можно вызывать для срабатывания всех загрузчиков данных экрана:

    @Subscribe
    public void onBeforeShow(BeforeShowEvent event) {
        getScreenData().loadAll();
    }
  • getSettings() - возвращает объект Settings, который может быть использован для чтения и записи специфичных настроек, ассоциированных с экраном для данного пользователя.

  • saveSettings() - сохраняет настройки экрана, представляемые объектом Settings. Данный метод вызывается автоматически, если свойство приложения cuba.gui.manualScreenSettingsSaving установлено в false (что является значением по умолчанию).

Методы StandardEditor
  • getEditedEntity() - когда экран открыт, возвращает экземпляр редактируемой сущности. Это экземпляр, установленный в контейнере данных, указанном аннотацией @EditedEntityContainer.

    В слушателях InitEvent и AfterInitEvent данный метод возвращает null. В слушателе BeforeShowEvent данный метод возвращает экземпляр, переданный в экран для редактирования (позднее в процессе открытия экрана сущность перезагружается, и другой ее экземпляр устанавливается в контейнере данных).

Для закрытия экрана редактирования можно использовать следующие методы:

  • closeWithCommit() - валидирует и сохраняет данные, затем закрывает экран с StandardOutcome.COMMIT.

  • closeWithDiscard() - игнорирует несохраненные изменения и закрывает экран с StandardOutcome.DISCARD.

Если в экране есть несохраненные изменения в DataContext, при закрытии экрана отображается соответствующее сообщение. Вид данного сообщения можно настроить с помощью свойства приложения cuba.gui.useSaveConfirmation. Если экран закрывается методом closeWithDiscard() или close(StandardOutcome.DISCARD) то несохраненные изменения игнорируются без каких-либо сообщений.

  • commitChanges() - сохраняет изменения не закрывая экран. Данный метод можно вызвать из собственного слушателя события (например нажатия кнопки), или переопределить слушатель действия windowCommit, чтобы выполнить какие-либо операции после сохранения данных. Пример переопределения стандартного действия windowCommit:

    @Override
    protected void commit(Action.ActionPerformedEvent event) {
        commitChanges().then(() -> {
            // this flag is used for returning correct outcome on subsequent screen closing
            commitActionPerformed = true;
            // perform actions after the data has been saved
        });
    }

    Стандартная реализация метода commit() отображает сообщение об успешном сохранении. Ее можно отключить, вызвав метод setShowSaveNotification(false) при инициализации экрана.

  • Метод validateAdditionalRules() можно переопределить для выполнения дополнительной валидации перед сохранением данных. Данный метод должен сохранить информацию об ошибках валидации в переданном ему объекте ValidationErrors. Впоследствии эта информация будет отображена вместе с ошибками стандартной процедуры валидации. Например:

private Pattern pattern = Pattern.compile("\\d");

@Override
protected void validateAdditionalRules(ValidationErrors errors) {
    if (getEditedEntity().getAddress().getCity() != null) {
        if (pattern.matcher(getEditedEntity().getAddress().getCity()).find()) {
            errors.add("City name cannot contain digits");
        }
    }
    super.validateAdditionalRules(errors);
}
Методы MasterDetailScreen
  • getEditedEntity() - когда когда находится в режиме редактирования, возвращает экземпляр редактируемой сущности. Это экземпляр, установленный в контейнере данных компонента form. Если экран не в режиме редактирования, данный метод выбрасывает IllegalStateException.

  • Метод validateAdditionalRules() можно переопределить для выполнения дополнительной валидации перед сохранением данных, как описано выше для StandardEditor.

3.5.1.1.3. События Screen

Ниже описаны события жизненного цикла экрана, на которые можно подписаться в контроллере для реализации необходимой бизнес-логики.

Руководство Decouple Business Logic with Application Events содержит примеры использования событий в UI.



InitEvent

InitEvent посылается, когда контроллер экрана и все его компоненты, заданные декларативно, созданы, а инжекция зависимостей завершена. Вложенные фрагменты на этом этапе ещё не инициализированы. Некоторые визуальные компоненты инициализированы не полностью: например, кнопки ещё не связаны с действиями.

@Subscribe
protected void onInit(InitEvent event) {
    Label<String> label = uiComponents.create(Label.TYPE_STRING);
    label.setValue("Hello World");
    getWindow().add(label);
}
AfterInitEvent

AfterInitEvent посылается, когда контроллер экрана и все его компоненты, заданные декларативно, созданы, инжекция зависимостей завершена, и все компоненты завершили свою внутреннюю процедуру инициализации. Вложенные фрагменты (при наличии) опубликовали свои события InitEvent и AfterInitEvent. В слушателе этого события можно создавать визуальные компоненты и компоненты данных, а также выполнить дополнительную инициализацию, если она зависит от инициализации вложенных фрагментов.

InitEntityEvent

InitEntityEvent посылается в экранах, унаследованных от StandardEditor и MasterDetailScreen, перед тем, как новый экземпляр сущности будет установлен для контейнера редактируемой сущности.

Руководство Initial Entity Values содержит пример инициализации сущности с помощью InitEntityEvent.

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

@Subscribe
protected void onInitEntity(InitEntityEvent<Foo> event) {
    event.getEntity().setStatus(Status.ACTIVE);
}
BeforeShowEvent

BeforeShowEvent посылается непосредственно перед тем, как экран будет отображён, иными словами, на этом этапе он ещё не добавлен к интерфейсу приложения. Ограничения безопасности уже применены к компонентам UI. Сохранённые настройки состояния компонентов UI ещё не применены. Данные ещё не загружены в экраны с аннотацией @LoadDataBeforeShow. В слушателе этого события можно загружать данные, проверять разрешения безопасности, а также изменять компоненты интерфейса. Например:

@Subscribe
protected void onBeforeShow(BeforeShowEvent event) {
    customersDl.load();
}
AfterShowEvent
  • AfterShowEvent посылается сразу после отображения экрана, то есть тогда, когда экран уже добавлен к интерфейсу приложения. На этом этапе применены сохранённые настройки состояния компонентов UI. В слушателе этого события можно отображать уведомления, диалоговые окна или другие экраны. Например:

@Subscribe
protected void onAfterShow(AfterShowEvent event) {
    notifications.create().withCaption("Just opened").show();
}
BeforeCommitChangesEvent

BeforeCommitChangesEvent посылается в экранах, унаследованных от StandardEditor и MasterDetailScreen, перед сохранением измененных данных методом commitChanges(). В слушателе этого события можно проверить какие-либо условия и прервать или продолжить операцию сохранения с помощью методов preventCommit() и resume() объекта события.