REST 数据存储
REST 数据存储(DataStore)的设计目的是为 Jmix 应用程序的集成提供一种简单的方法。期望的结果是能通过 DataManager 接口访问远程 Jmix 应用程序提供的外部实体,与操作本地 JPA 实体一样。在无需编写任何特定代码的前提下,将外部实体显示在本地 UI 中,并使用 Jmix 提供的标准 CRUD 功能更新并保存回远程应用程序。
本文档提供有关 REST 数据存储扩展组件的说明。关于其使用场景和更多内容,请参阅以下指南:
在本文档中,我们将使用以下术语:
-
服务端应用(Service Application) - 通过 通用 REST API 对外提供数据的 Jmix 应用程序(提供者)。
-
客户端应用(Client Application) - 通过 REST 数据存储使用服务端应用数据的 Jmix 应用程序(消费者)。
服务端和客户端应用可以使用不同版本的 Jmix。
安装
请按照 扩展组件 章节的介绍通过 Jmix 市场自动安装。
手动安装时,在 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 演示的内容使用真实的用户做服务端认证,那么需要在服务端先配置 密码授权,然后在客户端应用添加以下配置:
serviceapp.authenticator=restds_RestPasswordAuthenticator
jmix.restds.authentication-provider-store=serviceapp
UI 用户设置
如果需要通过 REST 数据存储保存用户的 UI 设置和过滤器设置,请按以下步骤:
-
在
build.gradle
中:-
添加
io.jmix.flowui:jmix-flowui-restds-starter
依赖 -
移除
io.jmix.flowui:jmix-flowui-data-starter
依赖
-
-
在
application.properties
中,通过jmix.restds.ui-config-store
属性配置 REST 数据存储的名称,示例:jmix.restds.ui-config-store=serviceapp
参阅应用程序分层指南中的 前端应用程序配置 部分了解更多。
REST API 路径
如果 REST API 的端点具有非标准的 路径,可以用下面展示的这些应用程序属性进行配置。这些属性以数据存储的名称开头(该示例中为 serviceapp
):
serviceapp.basePath=/rest
serviceapp.entitiesPath=/entities
serviceapp.userInfoPath=/userInfo
serviceapp.permissionsPath=/permissions
serviceapp.capabilitiesPath=/capabilities
数据模型
客户端应用需要包含与服务端实体完全匹配的 DTO 实体。为了实现实体和 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
注解,而不能用 JPA 的 @Embedded
。
客户端侧的一对多属性需要在 @Composition
注解中定义 inverse
属性,设置反向关联字段。
示例:
@Store(name = "serviceapp")
@JmixEntity
public class Customer {
// ...
@JmixEmbedded
@EmbeddedParameters(nullAllowed = false)
private Address address;
@Composition(inverse = "customer")
private Set<Contact> contacts;
// ...
Fetch Plans
在客户端应用中加载外部实体时,可以指定 fetch plan 来加载实体的引用。
如果服务端程序使用 Jmix 2.5 之前的版本构建,则通用 REST API 目前仅支持在 fetch plans 存储库中定义的 命名 fetch plans。因此,REST 数据存储在向服务端请求数据时,会同时提供 fetch plan 的名称。从而要求服务端和客户端应用程序必须在 fetch plan 存储库中定义所有的 fetch plan,并使用相同的名称。
从 Jmix 2.5 开始,通用 REST 开始支持 内联 fetch plans。如果该功能没有通过 jmix.rest.inline-fetch-plan-enabled 应用程序属性关闭,则可以像操作数据库一样,在视图的 XML 或 Java 代码中定义 fetch plan。
REST 数据存储依赖服务端 REST 的 能力 API 确定是否启用了内联 fetch plan。如果检查确定已启用,则 REST 数据存储会传递内联的 fetch plan,否则仅传输 fetch plan 的名称。
加载的数据
如果 fetch plan 不包含某个属性,则该属性不会加载。与 JPA 实体属性不同,未加载的 REST 实体的属性值为 null,并且在访问时不会触发任何异常。
更新实体时,REST 数据存储仅保存已加载的属性。如果某个属性未从服务端加载,但随后从 null 变为某个非 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();
}
使用查询
查询语句是一个通用 REST 搜索端点 支持的 JSON 表达式:
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 数据存储会在请求时将该属性替换为 "value": <parameter-value>
。
dataLoadCoordinator facet 也可以使用查询 JSON 和参数,但只支持手动配置。下面的示例中,regionsDc
和 customersDc
通过 JSON 查询进行关联,dataLoadCoordinator
为选择的 region 提供了一个 region 和 customer 的主从关系表:
<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 数据存储会发送 EntitySavingEvent 和 EntityLoadingEvent,这点与 JpaDataStore
一样。但是不会发送 EntityChangedEvent,因为在实体加载后无法跟踪实体属性的变化。尽管无法提供 EntityChangedEvent
,REST 数据存储能提供两个特殊的事件:
-
RestEntitySavedEvent
- 当实体在服务端成功保存后发送。事件中包含在发送给服务端之前的实体状态。 -
RestEntityRemovedEvent
- 当实体从服务端删除后发送。事件中包含在发送给服务端之前的已删除实体。
文件存储
服务端应用程序的实体可以包含指向其文件存储的 FileRef
属性。REST 数据存储提供了一个 RestFileStorage
,实现了 FileStorage
接口,这是一个代理类,可以处理服务端应用程序文件存储中的文件。
RestFileStorage
使用通用 REST 文件 API 在客户端和服务端程序之间传输文件。
使用 RestFileStorage
时,需要在客户端应用程序主类或 Spring 配置类中定义以下 bean:
@Bean
FileStorage serviceappFileStorage() {
return new RestFileStorage("serviceapp", "fs");
}
这里的 serviceapp
是 application.properties
中定义的 REST 数据存储的名称,fs
是服务端应用程序中需要使用的文件存储名称。
客户端应用程序中的 REST 文件存储的名称由这两个参数加短横拼接而成。所以对于上面的示例,这个名称是 serviceapp-fs
。如果如果客户端应用程序中有多个文件存储(比如,本地的 fs
和 远端的 serviceapp-fs
),那么请参阅 使用多个文件存储。
使用 RestFileStorage
的示例程序请参考 Separating Application Tiers 指南。
调用服务
RestDataStoreUtils
bean 提供了特定 REST 数据存储的 Spring RestClient
的引用。支持使用为 REST 数据存储配置的连接和身份验证参数来调用该服务端应用的任意 API 端点。
有关调用服务方法的示例,请参阅 Integrating Jmix Applications。
局限性
与 JPA 数据存储相比,REST 数据存储有下列限制:
-
不支持实体引用的延迟加载。fetch plan 中没有加载的实体引用在访问时会得到 null。
-
没有带
AttributeChanges
的EntityChangeEvent
事件。 -
DataManager.loadValues()
和loadValue()
方法会抛出UnsupportedOperationException
异常。