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

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

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

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

Клиентское приложение должно содержать 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;

    // ...

Фетч-планы

При загрузке внешней сущности в клиентском приложении можно указать фетч-план для загрузки ссылок. В настоящий момент универсальный 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.