领先一步
VMware 提供培训和认证,助您快速进步。
了解更多要查看此代码的更新,请访问我们的React.js 和 Spring Data REST 教程。 |
在上一个会话中,您了解了如何开启 Spring Data REST 的超媒体控制,如何通过分页进行 UI 导航,以及如何根据页大小更改动态调整大小。您添加了创建和删除员工并调整页面的能力。但是,任何解决方案都不完整,除非考虑到其他用户对您当前正在编辑的同一数据进行的更新。
欢迎从本仓库获取代码并跟随操作。本次会话基于上一次会话的应用,并增加了一些内容。
当您获取资源时,如果其他人对其进行了更新,则存在资源过时的风险。为了解决这个问题,Spring Data REST 集成了两种技术:资源版本控制和 ETags。
通过在后端进行资源版本控制并在前端使用 ETags,可以有条件地执行 PUT 操作。换句话说,您可以检测资源是否已更改,并阻止 PUT(或 PATCH)操作覆盖其他人的更新。我们来了解一下。
为了支持资源的版本控制,为需要此类保护的领域对象定义一个版本属性。
@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 检索每个单个资源。
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
});
});
},
follow()
函数会访问 employees 集合资源。then(employeeCollection ⇒ …)
子句创建了一个调用来获取 JSON Schema 数据。它有一个子 then 子句,用于将元数据和导航链接存储在 <App/>
组件中。then(employeeCollection ⇒ …)
子句将员工集合转换为一个 GET promises 数组,用于获取每个单个资源。这是获取每个员工的 ETag 头部所需的操作。then(employeePromises ⇒ …)
子句接受 GET promises 数组,并使用 when.all()
将它们合并为一个 promise,当所有 GET promises 都解析后,该 promise 也会解析。loadFromServer
以 done(employees ⇒ …)
结束,其中使用数据的这种合并来更新 UI 状态。此链也在其他地方实现。例如,用于跳到不同页面的 onNavigate()
已更新为获取单个资源。由于它与上面所示的大致相同,因此本次会话中未包含它。
在此会话中,您正在添加一个 UpdateDialog
React 组件来编辑现有员工记录。
var UpdateDialog = React.createClass({
handleSubmit: function (e) { e.preventDefault(); var updatedEmployee = {}; this.props.attributes.forEach(attribute => { 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 => <p key={this.props.employee.entity[attribute]}> <input type="text" placeholder={attribute} defaultValue={this.props.employee.entity[attribute]} ref={attribute} className="field" /> </p> ); var dialogId = "updateEmployee-" + this.props.employee.entity._links.self.href; return ( <div key={this.props.employee.entity._links.self.href}> <a href={"#" + dialogId}>Update</a> <div id={dialogId} className="modalDialog"> <div> <a href="#" title="Close" className="close">X</a> <h2>Update an employee</h2> <form> {inputs} <button onClick={this.handleSubmit}>Update</button> </form> </div> </div> </div> ) }
});
这个新组件既有 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 的单向绑定风格,其中要调用的函数从上层组件推送到下层组件。这样,状态仍然在顶部管理。
您已经付出了所有努力将版本控制嵌入到数据模型中。Spring Data REST 已将该值作为 ETag 响应头提供。现在您可以好好利用它了!
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 组件中看到它被插入
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 库时才相关。如果您使用其他库,则需要进行相应的调整。 |
./mvnw spring-boot:run
)。通过这些修改,您通过避免冲突提高了数据完整性。
在此会话中
@Version
字段的域模型。插入此功能后,可以轻松避免与其他用户冲突或简单地覆盖他们的编辑。
问题?
当然很高兴知道您正在编辑一个坏记录。但最好的方法是等到单击“提交”才发现吗?
在 loadFromServer
和 onNavigate
中获取资源的逻辑非常相似。您看到避免重复代码的方法了吗?
您充分利用了 JSON Schema 元数据来构建 CreateDialog
和 UpdateDialog
输入。您看到其他可以使用元数据使事情更通用的地方了吗?想象一下,您想向 Employee.java
添加五个额外的字段。更新 UI 需要什么?