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
Аутентификация с помощью 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 в сервисном и клиентском приложениях представлен ниже.
@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;
    // ...Фетч-планы
Когда вы загружаете внешнюю сущность в клиентском приложении, вы можете указать фетч-план для загрузки ссылок.
Если сервисное приложение построено с использованием 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-сервисы и десериализуют результаты.
В следующем примере показана реализация сервисного бина:
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.
Чтобы вызвать этот сервис из клиентского приложения, создайте следующий интерфейс:
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.
| Методы интерфейсов  
 | 
Использование RestClient
Вы также можете вызывать любые эндпойнты сервисного приложения с помощью Spring RestClient, предварительно настроенного параметрами подключения и аутентификации соответствующего REST-хранилища данных:
- 
Получите экземпляр RestClientиз бинаRestDataStoreUtilsс помощью методаgetRestClient(String dataStoreName).
- 
Используйте API RestClientдля вызова эндпойнта с соответствующими параметрами.
Ограничения
REST DataStore имеет следующие ограничения по сравнению с хранилищем данных JPA:
- 
Не поддерживает ленивую загрузку ссылок. Ссылки, которые не были загружены по фетч-плану, остаются null при доступе. 
- 
Отсутствует событие EntityChangeEventсAttributeChanges.
- 
Методы DataManager.loadValues()иloadValue()не реализованы и выбрасываютUnsupportedOperationException.