走在前面
VMware 提供培训和认证,以加快您的进度。
了解更多要查看此代码的更新,请访问我们的 React.js 和 Spring Data REST 教程。 |
在 上一节 中,您了解了如何开启 Spring Data REST 的超媒体控制,让 UI 通过分页进行导航,并根据页面大小的变化动态调整大小。您添加了创建和删除员工的功能,并让页面进行调整。但是,如果没有考虑到其他用户对您当前正在编辑的相同数据进行的更新,那么任何解决方案都不完整。
随时可以从这个仓库中 获取代码 并跟随操作。本节基于上一节的应用程序,并添加了一些额外内容。
当您获取资源时,如果其他人更新了它,则存在资源可能过时的风险。为了解决这个问题,Spring Data REST 集成了两种技术:资源版本控制和 ETag。
通过在后端对资源进行版本控制并在前端使用 ETag,可以有条件地执行 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 数据集被视为数据的预览。虽然它对于浏览数据很有用,但要获取像 ETag 这样的头信息,您需要单独获取每个资源。
在此版本中,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 promise 数组。这是获取每个员工的 ETag 头信息所必需的。then(employeePromises ⇒ …)
子句获取 GET promise 数组,并使用 when.all()
将它们合并到一个 promise 中,当所有 GET promise 都解析时,该 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 ref 提取弹出窗口的详细信息。
提取输入值并加载到 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 预期条件失败。
注意
|
Promises/A+ 的规范实际上将其 API 定义为 then(successFunction, errorFunction) 。到目前为止,您只看到它用于成功函数。在上面的代码片段中,有两个函数。成功函数调用 loadFromServer ,而错误函数显示有关过时数据的浏览器警报。 |
在定义并很好地链接到顶级 onUpdate
函数的 UpdateDialog
React 组件之后,最后一步是将其连接到现有组件的布局中。
上一节中创建的 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
,在那里我们可以找到 ETag。
Spring Data REST 支持其他头信息(如 Last-Modified),但它们不在本系列的讨论范围内。因此,以这种方式构造数据非常方便。
重要
|
.entity 和 .headers 的结构仅在使用 rest.js 作为 REST 库时才相关。如果您使用其他库,则需要根据需要进行调整。 |
./mvnw spring-boot:run
)。通过这些修改,您通过避免冲突提高了数据完整性。
在本节中
@Version
字段为基于 JPA 的乐观锁配置了域模型。通过插入此内容,可以轻松避免与其他用户发生冲突,或者简单地覆盖他们的编辑。
问题?
当然,知道您何时正在编辑错误的记录是很好的。但是,等到您单击“提交”才能发现是否最好呢?
获取资源的逻辑在 loadFromServer
和 onNavigate
中非常相似。您是否看到避免重复代码的方法?
您在构建 CreateDialog
和 UpdateDialog
输入时充分利用了 JSON Schema 元数据。您是否看到在其他地方使用元数据使事情更通用的方法?假设您想向 Employee.java
添加五个字段。更新 UI 需要做些什么?