REST DataStore

The purpose of the REST data store is to provide an easy way of integrating Jmix applications. The result of the integration is the ability to access external entities from a remote Jmix application through the DataManager interface in the same way as local JPA entities. The external entities can be displayed in UI, updated and saved back to the remote application using the standard CRUD functionality provided by Jmix, without writing any specific code.

This document provides reference information about the REST DataStore add-on. If you want to learn more about how to use it in various scenarios, refer to the following guides:

In this document, we will use the following terms:

  • Service Application - a Jmix application providing data through the generic REST API.

  • Client Application - a Jmix application consuming data from the Service Application using the REST data store.

The service and client applications can use different versions of Jmix.

Installation

For automatic installation through Jmix Marketplace, follow instructions in the Add-ons section.

For manual installation, add the following dependencies to your build.gradle:

implementation 'io.jmix.restds:jmix-restds-starter'

Configuration

The basic configuration includes the steps listed below.

In the service application project:

In the client application project:

  • Add the REST DataStore add-on as described above.

  • Add an additional data store with restds_RestDataStoreDescriptor descriptor, for example:

    jmix.core.additional-stores = serviceapp
    jmix.core.store-descriptor-serviceapp = restds_RestDataStoreDescriptor
  • Specify service connection properties for the data store by its name, for example:

    serviceapp.baseUrl = http://localhost:8081
    serviceapp.clientId = clientapp
    serviceapp.clientSecret = clientapp123

If you want to authenticate real users in the service application as demonstrated in Separating Application Tiers guide, set up the Password Grant in the service application and add the following properties to the client application:

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

Data Model

The client application should contain DTO entities that are equivalent to service entities. In order to be automatically mapped, the attributes of the entities must match by name and type.

The set of attributes may be different. For example, a service entity may have more attributes than a client entity. Attributes that are not present in an entity on the other side will have null values after data transfer.

The client DTO entity must have the @Store annotation specifying the additional data store.

The following example demonstrates the Region entity definition in the service and client applications.

Region entity in service application
@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 entity in client application
@Store(name = "serviceapp")
@JmixEntity
public class Region {
    @JmixGeneratedValue
    @JmixId
    private UUID id;

    private Integer version;

    @InstanceName
    @NotNull
    private String name;

    // getters and setters

If the client entity name differs from the service one, use the @RestDataStoreEntity annotation to specify the service entity name explicitly. For example:

@Store(name = "serviceapp")
@JmixEntity
@RestDataStoreEntity(remoteName = "Region")
public class RegionDto {
    // ...

For embedded attributes on the client side use the @JmixEmbedded annotation instead of JPA’s @Embedded.

For one-to-many composition attributes on the client side define the inverse attribute in the @Composition annotation.

For example:

@Store(name = "serviceapp")
@JmixEntity
public class Customer {
    // ...

    @JmixEmbedded
    @EmbeddedParameters(nullAllowed = false)
    private Address address;

    @Composition(inverse = "customer")
    private Set<Contact> contacts;

    // ...

Fetch Plans

When you load an external entity in the client application, you can specify a fetch plan to load references. The generic REST API currently supports only named fetch plans defined in fetch plans repository. So the REST data store will request data from the service providing a fetch plan name.

Therefore, both service and client applications must define all fetch plans in their fetch plan repositories, with corresponding names. Inline fetch plans in view XML and programmatically built fetch plans in Java are not supported.

Loaded State

If a fetch plan does not include an attribute, that attribute is not loaded. Unlike JPA entity attributes, the attributes of REST entities that are not loaded have a null value and do not throw any exceptions when accessed.

When updating an entity, the REST data store only saves loaded attributes. If an attribute was not loaded from the service but changed from null to some value afterwards, it is considered loaded and the new value is therefore saved.

The EntityStates.isLoaded(entity, property) method correctly returns information about whether a particular attribute of a REST entity is loaded.

Filtering Loaded Data

This section describes the filtering options supported when loading external entities using DataManager. All of these options lead to invoking the REST API search endpoint of the service application, so only the resulting entities are transferred over the wire.

By Conditions

For example:

List<Customer> loadByCondition(String lastName) {
    return dataManager.load(Customer.class)
            .condition(PropertyCondition.equal("lastName", lastName))
            .list();
}

By Query

The query is a JSON expression supported by generic REST in the search endpoint:

List<Customer> loadByQuery(String lastName) {
    String query = """
    {
      "property": "lastName",
      "operator": "=",
      "value": "%s"
    }
    """.formatted(lastName);

    return dataManager.load(Customer.class)
            .query(query)
            .list();
}

By Identifiers

For example:

List<Customer> loadByIdentifiers(UUID id1, UUID id2, UUID id3) {
    return dataManager.load(Customer.class)
            .ids(id1, id2, id3)
            .list();
}

Using Query in View XML

The JSON query can be specified in view XML descriptors for data containers and itemsQuery elements:

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

To specify a parameter instead of a literal value in JSON query conditions, use parameterName key instead of value as shown above. The REST data store will substitute this property with "value": <parameter-value> in the resulting request.

The dataLoadCoordinator facet can also be used, but only with manual configuration. In the following example, the regionsDc and customersDc data containers are linked using a JSON query and dataLoadCoordinator to provide a master-detail list of regions and customers for the selected region:

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

Entity Events

REST data store sends EntitySavingEvent and EntityLoadingEvent the same as JpaDataStore. But it doesn’t send EntityChangedEvent because it cannot provide information about attributes changed since load. Instead of EntityChangedEvent, REST data store sends two specific events:

  • RestEntitySavedEvent - sent after the entity is successfully saved to the service. It contains the saved entity instance with the state right before sending to the service.

  • RestEntityRemovedEvent - sent after the entity is removed from the service. It contains removed entity with the state right before sending to the service.

Security

REST data store applies entity operations policy defined by resource roles and predicate policy defined by row-level roles.

Authentication in REST data store can be done using Client Credentials Grant or Password Grant provided by the Authorization Server add-on. The latter requires setting the additional properties <ds-name>.authenticator and jmix.restds.authentication-provider-store as described in the Configuration section.

Invoking Services

The RestDataStoreUtils bean provides a reference to the Spring’s RestClient for a particular REST data store. It allows you to invoke arbitrary endpoints of that service application using connection and authentication parameters configured for the REST data store.

See an example of invoking a business service method in the Integrating Jmix Applications guide.

Limitations

The REST data store has the following limitations compared to the JPA data store:

  • Lazy loading of references is not supported. References that are not loaded by the fetch plan remain null when accessed.

  • There is no EntityChangeEvent with AttributeChanges.

  • DataManager.loadValues() and loadValue() methods throw UnsupportedOperationException.