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

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

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

随时可以从这个仓库中 获取代码 并跟随操作。本节基于上一节的应用程序,并添加了一些额外内容。

是否执行 PUT 操作,这是一个问题

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

通过在后端对资源进行版本控制并在前端使用 ETag,可以有条件地执行 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 数据集被视为数据的预览。虽然它对于浏览数据很有用,但要获取像 ETag 这样的头信息,您需要单独获取每个资源。

在此版本中,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 promise 数组。这是获取每个员工的 ETag 头信息所必需的。
  4. then(employeePromises ⇒ …​) 子句获取 GET promise 数组,并使用 when.all() 将它们合并到一个 promise 中,当所有 GET promise 都解析时,该 promise 就会解析。
  5. loadFromServer 使用 done(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 ref 提取弹出窗口的详细信息。

提取输入值并加载到 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 预期条件失败

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

综合起来

在定义并很好地链接到顶级 onUpdate 函数的 UpdateDialog React 组件之后,最后一步是将其连接到现有组件的布局中。

上一节中创建的 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,在那里我们可以找到 ETag。

Spring Data REST 支持其他头信息(如 Last-Modified),但它们不在本系列的讨论范围内。因此,以这种方式构造数据非常方便。

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

查看实际效果

  1. 启动应用程序(./mvnw spring-boot:run)。
  2. 打开一个选项卡并导航到 https://127.0.0.1:8080
    conditional 1
  3. 调出 Frodo 的编辑对话框。
  4. 在浏览器中打开另一个选项卡并调出相同的记录。
  5. 更改第一个选项卡中的记录。
  6. 尝试更改第二个选项卡中的记录。
    conditional 2
conditional 3

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

回顾

在本节中

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

通过插入此内容,可以轻松避免与其他用户发生冲突,或者简单地覆盖他们的编辑。

问题?

当然,知道您何时正在编辑错误的记录是很好的。但是,等到您单击“提交”才能发现是否最好呢?

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

您在构建 CreateDialogUpdateDialog 输入时充分利用了 JSON Schema 元数据。您是否看到在其他地方使用元数据使事情更通用的方法?假设您想向 Employee.java 添加五个字段。更新 UI 需要做些什么?

获取 Spring 电子邮件简报

通过 Spring 电子邮件简报保持联系

订阅

走在前面

VMware 提供培训和认证,以加快您的进度。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

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

查看全部