5.15. 数据模型版本化示例

实体属性被重命名

我们假设 sales$Order 实体的 oldNumber 属性被重命名为 newNumber 并且 date 被重命名为 deliveryDate。在这种情况下,转换配置将如下所示:

<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">
    <transformation modelVersion="1.0" currentEntityName="sales$Order">
        <renameAttribute oldName="oldNumber" currentName="newNumber"/>
        <renameAttribute oldName="date" currentName="deliveryDate"/>
    </transformation>
    ...
</transformations>

如果客户端应用程序需要使用旧版的 sales$Order 实体,那么它必须在 URL 参数中传递 modelVersion 值:

http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.0

将返回以下结果:

{
  "_entityName": "sales$Order",
  "_instanceName": "00001",
  "id": "46322d73-2374-1d65-a5f2-160461da22bf",
  "date": "2016-10-31",
  "description": "Vacation order",
  "oldNumber": "00001"
}

尽管 CUBA 应用程序中的实体具有 newNumberdeliveryDate 属性,但是响应 JSON 却包含 oldNumberdate 属性,

实体名称更改

接下来,试想一下,在应用程序的某个版本中,sales$Order 实体的名称也发生了变化。新名称是 sales$NewOrder

版本 1.1 的转换配置将如下所示:

<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">
    <transformation modelVersion="1.1" oldEntityName="sales$Order" currentEntityName="sales$NewOrder">
        <renameAttribute oldName="oldNumber" currentName="newNumber"/>
    </transformation>
    ...
</transformations>

除了上一个示例中的配置之外,还在此处添加了 oldEntityName 属性。它指定对模型版本 1.1 有效的实体名称。currentEntityName 属性指定当前实体名称。

虽然名称为 sales$Order 的实体不再存在,但以下请求也可以正常运行:

http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.1

REST API controller 会明白它必须在 sales$NewOrder 实体里进行搜索,并且在找到具有给定 id 的实体之后,在结果 JSON 中替换实体的名称和 newNumber 属性的名称:

{
  "_entityName": "sales$Order",
  "_instanceName": "00001",
  "id": "46322d73-2374-1d65-a5f2-160461da22bf",
  "date": "2016-10-31",
  "description": "Vacation order",
  "oldNumber": "00001"
}

客户端应用程序也可以使用旧版本的数据模型进行实体更新和创建。

POST 请求使用旧实体名称,并且在请求体使用旧的 JSON,这个 POST 请求可以正常工作:

http://localhost:8080/app/rest/v2/entities/sales$Order

{
  "_entityName": "sales$Order",
  "_instanceName": "00001",
  "id": "46322d73-2374-1d65-a5f2-160461da22bf",
  "date": "2016-10-31",
  "description": "Vacation order",
  "oldNumber": "00001"
}
必须从 JSON 中删除的实体属性

如果某个属性已添加到实体,但使用旧版数据模型的客户端不希望有此新属性,则可以从结果 JSON 中删除新属性。

此示例的转换配置如下所示:

<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">
    <transformation modelVersion="1.5" currentEntityName="sales$Order">
        <toVersion>
            <removeAttribute name="discount"/>
        </toVersion>
    </transformation>
    ...
</transformations>

此配置文件中的转换包含带有嵌套 removeAttribute 命令的 toVersion 标记。这意味着当执行从当前状态到特定版本的转换时(即当请求实体列表时),必须从结果 JSON 中删除 discount 属性。

在这种情况下,如果在没有 modelVersion 属性的情况下执行请求,discount 属性将会被返回:

http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93

{
    "_entityName": "sales$Order",
    "_instanceName": "00001",
    "id": "46322d73-2374-1d65-a5f2-160461da22bf",
    "deliveryDate": "2016-10-31",
    "description": "Vacation order",
    "number": "00001",
    "discount": 50
}

如果指定 modelVersion,则将删除 discount 属性

http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.1

{
    "_entityName": "sales$Order",
    "_instanceName": "00001",
    "id": "46322d73-2374-1d65-a5f2-160461da22bf",
    "deliveryDate": "2016-10-31",
    "description": "Vacation order",
    "oldNumber": "00001"
}
使用自定义转换器

还可以创建和注册自定义 JSON 转换器。举个例子,我们来看看下面的情况:有一个实体 sales$OldOrder 被重命名为 sales$NewOrder。该实体具有 orderDate 字段。在之前的版本中,此日期字段包含时间部分,但在最新版本的实体中,时间部分将被删除。请求旧模型版本 1.0 的实体的 REST API 客户端期望日期字段有时间部分,因此转换器必须修改 JSON 中的值。

首先,转换器配置必须如下所示:

<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">

    <transformation modelVersion="1.0" oldEntityName="sales$OldOrder" currentEntityName="sales$NewOrder">
        <custom>
            <fromVersion transformerBeanRef="sales_OrderJsonTransformerFromVersion"/>
            <toVersion transformerBeanRef="sales_OrderJsonTransformerToVersion"/>
        </custom>
    </transformation>

    ...
</transformations>

有一个 custom 元素和嵌套的 toVersionfromVersion 元素。这些元素引用了转换器 bean。这意味着自定义转换器必须注册为 Spring bean。这里有一件重要的事情:自定义转换器可以使用 RestTransformations 平台 bean(如果需要,这个 bean 可以访问其它实体转换器)。但是 RestTransformations bean 是在 REST API servlet 的 Spring 上下文中注册,而不是在 Web 应用程序的主上下文中注册。这意味着自定义转换器 bean 也必须在 REST API Spring 上下文中注册。

我们怎样做到这一点?

首先,在 webportal 模块中创建一个 rest-dispatcher-spring.xml (例如,在 com.company.test 包中)。

接下来,在 Web 或 portal 模块的 app.properties 中注册此文件:

cuba.restSpringContextConfig = +com/company/test/rest-dispatcher-spring.xml

rest-dispatcher-spring.xml 必须包含自定义转换器 bean 定义:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">

    <bean name="sales_OrderJsonTransformerFromVersion" class="com.company.test.transformer.OrderJsonTransformerFromVersion"/>
    <bean name="sales_OrderJsonTransformerToVersion" class="com.company.test.transformer.OrderJsonTransformerToVersion"/>

</beans>

sales_OrderJsonTransformerToVersion 转换器的内容如下:

package com.company.test.transformer;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import com.haulmont.restapi.transform.AbstractEntityJsonTransformer;
import com.haulmont.restapi.transform.JsonTransformationDirection;

public class OrderJsonTransformerToVersion extends AbstractEntityJsonTransformer {

    public OrderJsonTransformerToVersion() {
        super("sales$NewOrder", "sales$OldOrder", "1.0", JsonTransformationDirection.TO_VERSION);
    }

    @Override
    protected void doCustomTransformations(ObjectNode rootObjectNode, ObjectMapper objectMapper) {
        JsonNode orderDateNode = rootObjectNode.get("orderDate");
        if (orderDateNode != null) {
            String orderDateNodeValue = orderDateNode.asText();
            if (!Strings.isNullOrEmpty(orderDateNodeValue))
                rootObjectNode.put("orderDate", orderDateNodeValue + " 00:00:00.000");
        }
    }
}

此转换器在 JSON 对象中找到 orderDate 节点,并通过将时间部分添加到该值来修改该值。

当请求数据模型版本 1.0sales$OldOrder 实体时,结果 JSON 将包括含有时间部分的 orderDate 字段的实体,尽管它不再存储在数据库中。

再啰嗦几句关于定制转换器:它们必须实现 EntityJsonTransformer 接口。还可以继承 AbstractEntityJsonTransformer 类并覆盖其 doCustomTransformations 方法。AbstractEntityJsonTransformer 类包含标准转换器的所有功能。