React.js 和 Spring Data REST:第 3 部分 - 条件操作

工程 | Greg L. Turnquist | 2015 年 9 月 29 日 | ...
要查看此代码的更新,请访问我们的React.js 和 Spring Data REST 教程

上一个会话中,您了解了如何开启 Spring Data REST 的超媒体控制,如何通过分页进行 UI 导航,以及如何根据页大小更改动态调整大小。您添加了创建和删除员工并调整页面的能力。但是,任何解决方案都不完整,除非考虑到其他用户对您当前正在编辑的同一数据进行的更新。

欢迎从本仓库获取代码并跟随操作。本次会话基于上一次会话的应用,并增加了一些内容。

是 PUT 还是不 PUT,这是一个问题

当您获取资源时,如果其他人对其进行了更新,则存在资源过时的风险。为了解决这个问题,Spring Data REST 集成了两种技术:资源版本控制和 ETags。

通过在后端进行资源版本控制并在前端使用 ETags,可以有条件地执行 PUT 操作。换句话说,您可以检测资源是否已更改,并阻止 PUT(或 PATCH)操作覆盖其他人的更新。我们来了解一下。

版本化 REST 资源

为了支持资源的版本控制,为需要此类保护的领域对象定义一个版本属性。

src/main/java/com/greglturnquist/payroll/Employee.java
@Data
@Entity
public class Employee {
private @Id @GeneratedValue Long id;
private String firstName;
private String lastName;
private String description;

private @Version @JsonIgnore Long version;

private Employee() {}

public Employee(String firstName, String lastName, String description) {
	this.firstName = firstName;
	this.lastName = lastName;
	this.description = description;
}

}

  • version 字段使用 javax.persistence.Version 注解。这使得每次插入和更新行时都会自动存储和更新一个值。

当获取单个资源(而非集合资源)时,Spring Data REST 将自动添加一个ETag 响应头,其值为该字段的值。

获取单个资源及其头部

上一个会话中,您使用集合资源来收集数据并填充 UI 的 HTML 表格。对于 Spring Data REST,_embedded 数据集被视为数据的预览。虽然对于快速浏览数据很有用,但要获取 ETags 等头部,您需要单独获取每个资源。

在此版本中,loadFromServer 已更新,首先获取集合,然后使用 URI 检索每个单个资源。

src/main/resources/static/app.jsx - 获取每个资源
loadFromServer: function (pageSize) {
    follow(client, root, [
        {rel: 'employees', params: {size: pageSize}}]
    ).then(employeeCollection => {
        return client({
            method: 'GET',
            path: employeeCollection.entity._links.profile.href,
            headers: {'Accept': 'application/schema+json'}
        }).then(schema => {
            this.schema = schema.entity;
            this.links = employeeCollection.entity._links;
            return employeeCollection;
        });
    }).then(employeeCollection => {
        return employeeCollection.entity._embedded.employees.map(employee =>
                client({
                    method: 'GET',
                    path: employee._links.self.href
                })
        );
    }).then(employeePromises => {
        return when.all(employeePromises);
    }).done(employees => {
        this.setState({
            employees: employees,
            attributes: Object.keys(this.schema.properties),
            pageSize: pageSize,
            links: this.links
        });
    });
},
  1. follow() 函数会访问 employees 集合资源。
  2. then(employeeCollection ⇒ …​) 子句创建了一个调用来获取 JSON Schema 数据。它有一个子 then 子句,用于将元数据和导航链接存储在 <App/> 组件中。
    • 注意,这个嵌入式 promise 返回 employeeCollection。这样,可以将集合传递给下一个调用,同时让您在此过程中获取元数据。
  3. 第二个 then(employeeCollection ⇒ …​) 子句将员工集合转换为一个 GET promises 数组,用于获取每个单个资源。这是获取每个员工的 ETag 头部所需的操作。
  4. then(employeePromises ⇒ …​) 子句接受 GET promises 数组,并使用 when.all() 将它们合并为一个 promise,当所有 GET promises 都解析后,该 promise 也会解析。
  5. loadFromServerdone(employees ⇒ …​) 结束,其中使用数据的这种合并来更新 UI 状态。

此链也在其他地方实现。例如,用于跳到不同页面的 onNavigate() 已更新为获取单个资源。由于它与上面所示的大致相同,因此本次会话中未包含它。

更新现有资源

在此会话中,您正在添加一个 UpdateDialog React 组件来编辑现有员工记录。

src/main/resources/static/app.jsx - UpdateDialog 组件
var UpdateDialog = React.createClass({
handleSubmit: function (e) {
    e.preventDefault();
    var updatedEmployee = {};
    this.props.attributes.forEach(attribute =&gt; {
        updatedEmployee[attribute] = React.findDOMNode(this.refs[attribute]).value.trim();
    });
    this.props.onUpdate(this.props.employee, updatedEmployee);
    window.location = "#";
},

render: function () {
    var inputs = this.props.attributes.map(attribute =&gt;
            &lt;p key={this.props.employee.entity[attribute]}&gt;
                &lt;input type="text" placeholder={attribute}
                       defaultValue={this.props.employee.entity[attribute]}
                       ref={attribute} className="field" /&gt;
            &lt;/p&gt;
    );

    var dialogId = "updateEmployee-" + this.props.employee.entity._links.self.href;

    return (
        &lt;div key={this.props.employee.entity._links.self.href}&gt;
            &lt;a href={"#" + dialogId}&gt;Update&lt;/a&gt;
            &lt;div id={dialogId} className="modalDialog"&gt;
                &lt;div&gt;
                    &lt;a href="#" title="Close" className="close"&gt;X&lt;/a&gt;

                    &lt;h2&gt;Update an employee&lt;/h2&gt;

                    &lt;form&gt;
                        {inputs}
                        &lt;button onClick={this.handleSubmit}&gt;Update&lt;/button&gt;
                    &lt;/form&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    )
}

});

这个新组件既有 handleSubmit() 函数,也有预期的 render() 函数,类似于 <CreateDialog /> 组件。

让我们反过来深入研究这些函数,首先查看 render() 函数。

渲染

此组件使用与上一次会话中的 <CreateDialog /> 相同的 CSS/HTML 策略来显示和隐藏对话框。

它将 JSON Schema 属性数组转换为 HTML 输入数组,并用段落元素包装以进行样式设置。这也与 <CreateDialog /> 相同,但有一个区别:字段是用 this.props.employee 加载的。在 CreateDialog 组件中,字段是空的。

id 字段的构建方式不同。整个 UI 上只有一个 CreateDialog 链接,但显示的每一行都有一个单独的 UpdateDialog 链接。因此,id 字段基于 self 链接的 URI。这用于 <div> 元素的 React key 以及 HTML 锚标签和隐藏的弹出窗口中。

处理用户输入

提交按钮链接到组件的 handleSubmit() 函数。它巧妙地使用 React.findDOMNode() 来使用React refs 提取弹出窗口的详细信息。

提取输入值并加载到 updatedEmployee 对象后,会调用顶层 onUpdate() 方法。这延续了 React 的单向绑定风格,其中要调用的函数从上层组件推送到下层组件。这样,状态仍然在顶部管理。

有条件的 PUT

您已经付出了所有努力将版本控制嵌入到数据模型中。Spring Data REST 已将该值作为 ETag 响应头提供。现在您可以好好利用它了!

src/main/resources/static/app.jsx - onUpdate 函数
onUpdate: function (employee, updatedEmployee) {
    client({
        method: 'PUT',
        path: employee.entity._links.self.href,
        entity: updatedEmployee,
        headers: {
            'Content-Type': 'application/json',
            'If-Match': employee.headers.Etag
        }
    }).done(response => {
        this.loadFromServer(this.state.pageSize);
    }, response => {
        if (response.status.code === 412) {
            alert('DENIED: Unable to update ' +
                employee.entity._links.self.href + '. Your copy is stale.');
        }
    });
},

带有If-Match 请求头的 PUT 操作会使 Spring Data REST 检查该值与当前版本是否匹配。如果传入的 If-Match 值与数据存储的版本值不匹配,Spring Data REST 将失败并返回 HTTP 412 Precondition Failed

注意
Promises/A+ 的规范实际上将它们的 API 定义为 then(successFunction, errorFunction)。到目前为止,您只见过它与成功函数一起使用。在上面的代码片段中,有两个函数。成功函数调用 loadFromServer,而错误函数显示一个关于数据过时的浏览器警告。

整合所有内容

定义了您的 UpdateDialog React 组件并将其 nicely 链接到顶层 onUpdate 函数后,最后一步是将其连接到现有组件布局中。

上一次会话中创建的 CreateDialog 放置在 EmployeeList 的顶部,因为只有一个实例。然而,UpdateDialog 直接与特定员工相关联。所以您可以在下面的 Employee React 组件中看到它被插入

src/main/resources/static/app.jsx - 带 UpdateDialog 的 Employee
var Employee = React.createClass({
    handleDelete: function () {
        this.props.onDelete(this.props.employee);
    },
    render: function () {
        return (
            <tr>
                <td>{this.props.employee.entity.firstName}</td>
                <td>{this.props.employee.entity.lastName}</td>
                <td>{this.props.employee.entity.description}</td>
                <td>
                    <UpdateDialog employee={this.props.employee}
                                  attributes={this.props.attributes}
                                  onUpdate={this.props.onUpdate}/>
                </td>
                <td>
                    <button onClick={this.handleDelete}>Delete</button>
                </td>
            </tr>
        )
    }
})

在此会话中,您从使用集合资源切换到使用单个资源。员工记录的字段现在位于 this.props.employee.entity。这使我们可以访问 this.props.employee.headers,在那里我们可以找到 ETags。

Spring Data REST 支持的其他头部(如Last-Modified)不属于本系列。因此,以这种方式构造数据非常方便。

重要事项
.entity.headers 的结构仅在使用 rest.js 作为首选 REST 库时才相关。如果您使用其他库,则需要进行相应的调整。

查看实际效果

  1. 启动应用程序(./mvnw spring-boot:run)。
  2. 打开一个标签页,导航到http://localhost:8080
    conditional 1
  3. 拉起 Frodo 的编辑对话框。
  4. 在浏览器中打开另一个标签页,拉起相同的记录。
  5. 在第一个标签页中更改记录。
  6. 尝试在第二个标签页中进行更改。
    conditional 2
conditional 3

通过这些修改,您通过避免冲突提高了数据完整性。

回顾

在此会话中

  • 您为基于 JPA 的乐观锁配置了带有 @Version 字段的域模型。
  • 您调整了前端以获取单个资源。
  • 您将单个资源的 ETag 头部插入到 If-Match 请求头部中,以使 PUT 操作具有条件性。
  • 您为列表上显示的每个员工编写了一个新的 UpdateDialog。

插入此功能后,可以轻松避免与其他用户冲突或简单地覆盖他们的编辑。

问题?

当然很高兴知道您正在编辑一个坏记录。但最好的方法是等到单击“提交”才发现吗?

loadFromServeronNavigate 中获取资源的逻辑非常相似。您看到避免重复代码的方法了吗?

您充分利用了 JSON Schema 元数据来构建 CreateDialogUpdateDialog 输入。您看到其他可以使用元数据使事情更通用的地方了吗?想象一下,您想向 Employee.java 添加五个额外的字段。更新 UI 需要什么?

获取 Spring 新闻稿

订阅 Spring 新闻稿,保持联系

订阅

领先一步

VMware 提供培训和认证,助您快速进步。

了解更多

获取支持

Tanzu Spring 通过一项简单的订阅即可为 OpenJDK™、Spring 和 Apache Tomcat® 提供支持和二进制文件。

了解更多

即将举办的活动

查看 Spring 社区所有即将举办的活动。

查看全部