REST DataStore
Задача REST DataStore — предоставить простой способ интеграции приложений Jmix. В результате такой интеграции появляется возможность получать доступ к внешним сущностям из удалённого приложения Jmix через интерфейс DataManager таким же образом, как к локальным сущностям JPA. Внешние сущности могут отображаться в пользовательском интерфейсе, обновляться и сохраняться обратно в удалённое приложение с использованием стандартной CRUD-функциональности, предоставляемой Jmix, без написания дополнительного кода.
Данный раздел предоставляет справочную информацию о дополнении REST DataStore. Для получения информации о том, как использовать его в различных сценариях, обратитесь к следующим руководствам:
В данном документе используются следующие термины:
-
Сервисное приложение — приложение Jmix, предоставляющее данные через универсальный REST API.
-
Клиентское приложение — приложение Jmix, получающее данные от Сервисного приложения с использованием REST DataStore.
Сервисное и клиентское приложения могут использовать разные версии Jmix.
Установка
Для автоматической установки через Jmix Marketplace следуйте инструкциям в разделе Дополнения.
Для ручной установки добавьте следующие зависимости в ваш build.gradle
:
implementation 'io.jmix.restds:jmix-restds-starter'
Конфигурация
Базовая конфигурация включает следующие шаги.
В проекте сервисного приложения:
-
Добавьте дополнения REST API и Authorization Server.
-
Настройте выдачу токенов с типом гранта Client Credentials.
В проекте клиентского приложения:
-
Добавьте дополнение REST DataStore, как описано выше.
-
Добавьте дополнительное хранилище данных с дескриптором
restds_RestDataStoreDescriptor
, например:jmix.core.additional-stores = serviceapp jmix.core.store-descriptor-serviceapp = restds_RestDataStoreDescriptor
-
Укажите свойства подключения к сервису для данного хранилища данных по его имени, например:
serviceapp.baseUrl = http://localhost:8081 serviceapp.clientId = clientapp serviceapp.clientSecret = clientapp123
Если вы хотите выполнять аутентификацию реальных пользователей в сервисном приложении, как продемонстрировано в руководстве Separating Application Tiers, настройте Password Grant в сервисном приложении и добавьте следующие свойства в клиентское приложение:
serviceapp.authenticator = restds_RestPasswordAuthenticator
jmix.restds.authentication-provider-store = serviceapp
Модель данных
Клиентское приложение должно содержать DTO-сущности, эквивалентные сущностям сервисного приложения. Для автоматического сопоставления атрибуты этих сущностей должны совпадать по имени и типу.
Набор атрибутов может различаться. Например, сервисная сущность может иметь больше атрибутов, чем клиентская. Атрибуты, отсутствующие в одной из сущностей, будут иметь значение null после передачи данных.
Клиентская DTO-сущность должна иметь аннотацию @Store
, указывающую на дополнительное хранилище данных.
Пример определения сущности Region
в сервисном и клиентском приложениях представлен ниже.
@JmixEntity
@Table(name = "REGION")
@Entity
public class Region {
@JmixGeneratedValue
@Column(name = "ID", nullable = false)
@Id
private UUID id;
@Column(name = "VERSION", nullable = false)
@Version
private Integer version;
@InstanceName
@Column(name = "NAME", nullable = false)
@NotNull
private String name;
// getters and setters
@Store(name = "serviceapp")
@JmixEntity
public class Region {
@JmixGeneratedValue
@JmixId
private UUID id;
private Integer version;
@InstanceName
@NotNull
private String name;
// getters and setters
Если имя клиентской сущности отличается от сервисного, используйте аннотацию @RestDataStoreEntity
для явного указания имени сущности в сервисе. Например:
@Store(name = "serviceapp")
@JmixEntity
@RestDataStoreEntity(remoteName = "Region")
public class RegionDto {
// ...
Для вложенных атрибутов на стороне клиента используйте аннотацию @JmixEmbedded
вместо @Embedded
из JPA.
Для атрибутов композиции с типом один-ко-многим на стороне клиента определите атрибут inverse
в аннотации @Composition
.
Пример:
@Store(name = "serviceapp")
@JmixEntity
public class Customer {
// ...
@JmixEmbedded
@EmbeddedParameters(nullAllowed = false)
private Address address;
@Composition(inverse = "customer")
private Set<Contact> contacts;
// ...
Фетч-планы
При загрузке внешней сущности в клиентском приложении можно указать фетч-план для загрузки ссылок. В настоящий момент универсальный REST API поддерживает только именованные фетч-планы, определённые в репозитории фетч-планов. Таким образом, REST DataStore запрашивает данные из сервиса, предоставляя имя фетч-плана.
Поэтому и сервисное, и клиентское приложения должны определять все фетч-планы в своих репозиториях с соответствующими именами. Фетч-планы, встроенные в XML-дескрипторы, а также программно создаваемые на Java фетч-планы не поддерживаются.
Загруженное состояние
Если фетч-план не включает какой-либо атрибут, этот атрибут не загружается. В отличие от атрибутов сущностей JPA, атрибуты REST-сущностей, которые не были загружены, имеют значение null и не вызывают исключений при доступе к ним.
При обновлении сущности REST DataStore сохраняет только загруженные атрибуты. Если атрибут не был загружен из сервиса, но впоследствии изменён с null на какое-либо значение, он считается загруженным, и новое значение будет сохранено.
Метод EntityStates.isLoaded(entity, property)
корректно возвращает информацию о том, загружен ли определённый атрибут REST-сущности.
Фильтрация загружаемых данных
Данный подраздел описывает параметры фильтрации, поддерживаемые при загрузке внешних сущностей с помощью DataManager
. Все эти опции приводят к вызову эндпойнта поиска REST API сервисного приложения, поэтому по сети передаются только найденные сущности.
По условиям
Пример:
List<Customer> loadByCondition(String lastName) {
return dataManager.load(Customer.class)
.condition(PropertyCondition.equal("lastName", lastName))
.list();
}
По запросу
Запрос представляет собой выражение на JSON, поддерживаемое универсальным REST API в эндпойнте поиска:
List<Customer> loadByQuery(String lastName) {
String query = """
{
"property": "lastName",
"operator": "=",
"value": "%s"
}
""".formatted(lastName);
return dataManager.load(Customer.class)
.query(query)
.list();
}
По идентификаторам
Пример:
List<Customer> loadByIdentifiers(UUID id1, UUID id2, UUID id3) {
return dataManager.load(Customer.class)
.ids(id1, id2, id3)
.list();
}
Использование запроса в XML экранов
JSON-запрос может быть указан в XML-дескрипторах экранов для контейнеров данных и элементов itemsQuery
:
<entityComboBox id="regionField" property="region">
<itemsQuery class="com.company.clientapp.entity.Region"
searchStringFormat="${inputString}">
<fetchPlan extends="_base"/>
<query>
<![CDATA[
{
"property": "name",
"operator": "contains",
"parameterName": "searchString"
}
]]>
</query>
</itemsQuery>
</entityComboBox>
Чтобы указать параметр вместо фиксированного значения в JSON-условиях запроса, используйте ключ parameterName
вместо value
, как показано выше. REST DataStore заменит это свойство на "value": <parameter-value>
в результирующем запросе.
Также можно использовать фасет dataLoadCoordinator, но только с ручной конфигурацией. В следующем примере контейнеры данных regionsDc
и customersDc
связаны, используя JSON-запрос и dataLoadCoordinator
, для предоставления в режиме мастер-деталь списка регионов и клиентов для выбранного региона:
<data>
<collection id="regionsDc"
class="com.company.clientapp.entity.Region">
<loader id="regionsDl" readOnly="true"/>
</collection>
<collection id="customersDc" class="com.company.clientapp.entity.Customer">
<fetchPlan extends="_base"/>
<loader id="customersDl" readOnly="true">
<query>
<![CDATA[
{
"property": "region",
"operator": "=",
"parameterName": "region"
}
]]>
</query>
</loader>
</collection>
</data>
<facets>
<dataLoadCoordinator>
<refresh loader="regionsDl">
<onViewEvent type="BeforeShow"/>
</refresh>
<refresh loader="customersDl">
<onContainerItemChanged container="regionsDc" param="region"/>
</refresh>
</dataLoadCoordinator>
<!-- ... -->
События сущностей
REST DataStore отправляет события EntitySavingEvent и EntityLoadingEvent так же, как JpaDataStore
. Однако оно не отправляет событие EntityChangedEvent, так как не может предоставить информацию об изменённых с момента загрузки атрибутах. Вместо события EntityChangedEvent
REST DataStore отправляет два специфических события:
-
RestEntitySavedEvent
— отправляется после успешного сохранения сущности в сервисе. Оно содержит сохранённый экземпляр сущности в состоянии перед отправкой в сервис. -
RestEntityRemovedEvent
— отправляется после удаления сущности из сервиса. Оно содержит удалённую сущность в состоянии перед отправкой в сервис.
Безопасность
REST DataStore применяет политику операций над сущностями, определённую ресурсными ролями, и политику предикатов, определённую ролями уровня строк.
Аутентификация в REST DataStore может выполняться с использованием Client Credentials Grant или Password Grant, предоставляемых дополнением Authorization Server. Последний вариант требует настройки дополнительных свойств <ds-name>.authenticator
и jmix.restds.authentication-provider-store
, как описано в разделе Конфигурация.
Вызов сервисов
Бин RestDataStoreUtils
предоставляет ссылку на Spring RestClient
для конкретного REST DataStore. Он позволяет вызывать произвольные эндпойнты сервисного приложения, используя параметры подключения и аутентификации, настроенные для REST DataStore.
Пример вызова метода бизнес-сервиса приведён в руководстве Integrating Jmix Applications.
Ограничения
REST DataStore имеет следующие ограничения по сравнению с хранилищем данных JPA:
-
Не поддерживает ленивую загрузку ссылок. Ссылки, которые не были загружены по фетч-плану, остаются null при доступе.
-
Отсутствует событие
EntityChangeEvent
сAttributeChanges
. -
Методы
DataManager.loadValues()
иloadValue()
не реализованы и выбрасываютUnsupportedOperationException
.