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:
-
Add REST API and Authorization Server add-ons.
-
Set up Client Credentials grant for a client.
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.
@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
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
withAttributeChanges
. -
DataManager.loadValues()
andloadValue()
methods throwUnsupportedOperationException
.