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

Аутентификация с помощью Password Grant

Если вы хотите выполнять аутентификацию реальных пользователей в сервисном приложении, как продемонстрировано в руководстве Separating Application Tiers, настройте Password Grant в сервисном приложении и добавьте следующие свойства в клиентское приложение:

serviceapp.authenticator=restds_RestPasswordAuthenticator
jmix.restds.authentication-provider-store=serviceapp

Внешняя аутентификация

Если вы хотите использовать внешний провайдер аутентификации (например, Keycloak) вместо Jmix Authorization Server, удалите дополнение Authorization Server из проекта и добавьте дополнение OpenID Connect.

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

serviceapp.authenticator=restds_RestOAuth2ClientAuthenticator
serviceapp.oauth2-client-registration=keycloak
  • Свойство [datastore-name].authenticator определяет имя бина, который реализует интерфейс RestAuthenticator и используется для аутентификации запросов к REST API.

    Бин restds_RestOAuth2ClientAuthenticator использует OAuth2-токен текущего пользователя.

  • Свойство [datastore-name].oauth2-client-registration определяет ID регистрации клиента OAuth2 в Spring Security, который используется в свойствах spring.security.oauth2.client.registration.[ID] для подключения к OIDC-провайдеру.

    Например, если вы подключаетесь к Keycloak с помощью этого свойства:

    spring.security.oauth2.client.registration.keycloak.client-id=integrated-apps-client

    Тогда значение свойства [datastore-name].oauth2-client-registration должно быть keycloak.

Полный пример см. в руководстве REST DataStore with External Authentication.

UI-настройки пользователей

Чтобы сохранять UI-настройки пользователей и конфигурации фильтров через REST-хранилище данных, выполните следующие действия:

  • В build.gradle:

    • добавьте зависимость io.jmix.flowui:jmix-flowui-restds-starter

    • удалите зависимость io.jmix.flowui:jmix-flowui-data-starter

  • В application.properties укажите имя REST-хранилища данных в свойстве jmix.restds.ui-config-store, например:

    jmix.restds.ui-config-store=serviceapp

См. также Frontend Application Configuration в руководстве Separating Application Tiers.

REST API Paths

Если эндпойнты REST API имеют нестандартные пути, вы можете настроить доступ к ним, используя свойства приложения, показанные ниже. Свойства начинаются с имени хранилища данных (serviceapp в данном примере):

serviceapp.basePath=/rest
serviceapp.entitiesPath=/entities
serviceapp.userInfoPath=/userInfo
serviceapp.permissionsPath=/permissions
serviceapp.capabilitiesPath=/capabilities

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

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

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

Клиентская DTO-сущность должна иметь аннотацию @Store, указывающую на дополнительное хранилище данных.

Пример определения сущности Region в сервисном и клиентском приложениях представлен ниже.

Сущность 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
Сущность Region в клиентском приложении
@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;

    // ...

Фетч-планы

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

Если сервисное приложение построено с использованием Jmix до версии 2.5, его универсальный REST API поддерживает только именованные фетч-планы, определенные в репозитории фетч-планов. Таким образом, REST DataStore будет запрашивать данные у сервиса, предоставляя имя фетч-плана. Следовательно, как сервисные, так и клиентские приложения должны определять все фетч-планы в своих репозиториях фетч-планов с соответствующими именами.

Начиная с Jmix 2.5, универсальный REST поддерживает встроенные (inline) фетч-планы, если эта функция не отключена свойством jmix.rest.inline-fetch-plan-enabled. Таким образом, вы можете определять фетч-планы в XML экрана и Java-коде вашего клиента так же, как и при работе с базами данных.

REST DataStore полагается на Capabilities API REST сервисного приложения, чтобы определить, включены ли встроенные фетч-планы. Если проверка подтверждает их доступность, REST DataStore передает встроенные фетч-планы, в противном случае передаются только имена фетч-планов.

Загруженное состояние

Если фетч-план не включает какой-либо атрибут, этот атрибут не загружается. В отличие от атрибутов сущностей 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, как описано в разделе Конфигурация.

Хранилище файлов

Сущности сервисного приложения могут содержать ссылки FileRef на файлы в своих хранилищах файлов. REST DataStore дополнение предоставляет RestFileStorage реализацию интерфейса FileStorage, который является прокси, позволяющим работать с файлами, расположенными в хранилищах файлов сервисного приложения.

RestFileStorage использует общий REST Files API для передачи файлов между клиентом и сервисным приложением.

Чтобы использовать RestFileStorage в вашем клиентском приложении, определите следующий бин в главном классе приложения или в любом другом классе Spring configuration:

@Bean
FileStorage serviceappFileStorage() {
    return new RestFileStorage("serviceapp", "fs");
}

Здесь serviceapp - это имя REST DataStore, как определено в application.properties, fs - это имя соответствующего хранилища файлов сервисного приложения.

Имя REST хранилища файлов в клиентском приложении формируется путем объединения этих параметров с дефисом между ними. Таким образом, в приведенном выше примере это будет serviceapp-fs. Если у вас есть несколько хранилищ файлов в клиентском приложении (скажем, локальный fs и удаленный serviceapp-fs), см. Использование нескольких хранилищ файлов для получения дополнительной информации.

Пример работы с RestFileStorage приведён в руководстве Separating Application Tiers.

Вызов сервисов

Если сервисное приложение предоставляет Java-сервис через REST API, клиентские приложения могут его использовать.

Интерфейсы @RemoteService

Самый простой способ вызвать сервис — создать интерфейс, который отражает методы сервиса, и пометить его аннотацией @RemoteService. Для интерфейсов с этой аннотацией дополнение REST DataStore генерирует клиентские прокси-объекты. Эти объекты автоматически сериализуют параметры, вызывают удалённые REST-сервисы и десериализуют результаты.

В следующем примере показана реализация сервисного бина:

CustomerService.java в сервисном приложении
package com.company.serviceapp.service;

import com.company.serviceapp.entity.Customer;
import io.jmix.core.DataManager;
import io.jmix.rest.annotation.RestMethod;
import io.jmix.rest.annotation.RestService;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

@RestService("customers")
public class CustomerService {

    @Autowired
    private DataManager dataManager;

    @RestMethod
    public List<Customer> getCustomersByName(String name) {
        return dataManager.load(Customer.class).query("e.name = ?1", name).list();
    }
}

Метод сервиса доступен через эндпойнт /rest/services/customers/getCustomersByName.

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

CustomerService.java в клиентском приложении
package com.company.clientapp.service;

import com.company.clientapp.entity.Customer;
import io.jmix.restds.annotation.RemoteService;

import java.util.List;

@RemoteService(store = "serviceapp", remoteName = "customers")
public interface CustomerService {

    List<Customer> getCustomersByName(String name);
}

Дополнение REST DataStore автоматически создаст бин для этого интерфейса, так что вы сможете инжектировать его в экран или бин и использовать как обычный Spring-бин. Например:

@Autowired
private CustomerService customerService;

private void processCustomers(String name) {
    List<Customer> customers = customerService.getCustomersByName(name);
    // ...
}

Другой пример вызова метода бизнес-сервиса можно найти в руководстве Integrating Jmix Applications.

Методы интерфейсов @RemoteService могут иметь параметры и возвращать результаты следующих типов:

  • Простые типы: примитивы (int, double и т.д.), обёртки (Integer, Double и т.д.), String.

  • Сущность

  • Перечисление

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

  • Произвольный POJO или Java-record с полями всех вышеперечисленных типов.

  • java.util.List, параметризованный любым из вышеперечисленных типов.

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

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

  1. Получите экземпляр RestClient из бина RestDataStoreUtils с помощью метода getRestClient(String dataStoreName).

  2. Используйте API RestClient для вызова эндпойнта с соответствующими параметрами.

Ограничения

REST DataStore имеет следующие ограничения по сравнению с хранилищем данных JPA:

  • Не поддерживает ленивую загрузку ссылок. Ссылки, которые не были загружены по фетч-плану, остаются null при доступе.

  • Отсутствует событие EntityChangeEvent с AttributeChanges.

  • Методы DataManager.loadValues() и loadValue() не реализованы и выбрасывают UnsupportedOperationException.