Conditional Operations with Headers
This section shows how Spring Data REST uses standard HTTP headers to enhance performance, conditionalize operations, and contribute to a more sophisticated frontend.
ETag
, If-Match
, and If-None-Match
Headers
The ETag
header provides a way to tag resources. This can prevent clients from overriding each other while also making it possible to reduce unnecessary calls.
Consider the following example:
class Sample {
@Version Long version; (1)
Sample(Long version) {
this.version = version;
}
}
1 | The @Version annotation (the JPA one in case you’re using Spring Data JPA, the Spring Data org.springframework.data.annotation.Version one for all other modules) flags this field as a version marker. |
The POJO in the preceding example, when served up as a REST resource by Spring Data REST, has an ETag
header with the value of the version field.
We can conditionally PUT
, PATCH
, or DELETE
that resource if we supply a If-Match
header such as the following:
curl -v -X PATCH -H 'If-Match: <value of previous ETag>' ...
Only if the resource’s current ETag
state matches the If-Match
header is the operation carried out. This safeguard prevents clients from stomping on each other. Two different clients can fetch the resource and have an identical ETag
. If one client updates the resource, it gets a new ETag
in the response. But the first client still has the old header. If that client attempts an update with the If-Match
header, the update fails because they no longer match. Instead, that client receives an HTTP 412 Precondition Failed
message to be sent back. The client can then catch up however is necessary.
The term, “version,” may carry different semantics with different data stores and even different semantics within your application. Spring Data REST effectively delegates to the data store’s metamodel to discern if a field is versioned and, if so, only allows the listed updates if ETag elements match.
|
The If-None-Match
header provides an alternative. Instead of conditional updates, If-None-Match
allows conditional queries. Consider the following example:
curl -v -H 'If-None-Match: <value of previous etag>' ...
The preceding command (by default) runs a GET
. Spring Data REST checks for If-None-Match
headers while doing a GET
. If the header matches the ETag, it concludes that nothing has changed and, instead of sending a copy of the resource, sends back an HTTP 304 Not Modified
status code. Semantically, it reads “If this supplied header value does not match the server-side version, send the whole resource. Otherwise, do not send anything.”
This POJO is from an ETag -based unit test, so it does not have @Entity (JPA) or @Document (MongoDB) annotations, as expected in application code. It focuses solely on how a field with @Version results in an ETag header.
|
If-Modified-Since
header
The If-Modified-Since
header provides a way to check whether a resource has been updated since the last request, which lets applications avoid resending the same data. Consider the following example:
@Document
public class Receipt {
public @Id String id;
public @Version Long version;
public @LastModifiedDate Date date; (1)
public String saleItem;
public BigDecimal amount;
}
1 | Spring Data Commons’s @LastModifiedDate annotation allows capturing this information in multiple formats (JodaTime’s DateTime , legacy Java Date and Calendar , JDK8 date/time types, and long /Long ). |
With the date field in the preceding example, Spring Data REST returns a Last-Modified
header similar to the following:
Last-Modified: Wed, 24 Jun 2015 20:28:15 GMT
This value can be captured and used for subsequent queries to avoid fetching the same data twice when it has not been updated, as the following example shows:
curl -H "If-Modified-Since: Wed, 24 Jun 2015 20:28:15 GMT" ...
With the preceding command, you are asking that a resource be fetched only if it has changed since the specified time. If so, you get a revised Last-Modified
header with which to update the client. If not, you receive an HTTP 304 Not Modified
status code.
The header is perfectly formatted to send back for a future query.
Do not mix and match header value with different queries. Results could be disastrous. Use the header values ONLY when you request the exact same URI and parameters. |
Architecting a More Efficient Front End
ETag
elements, combined with the If-Match
and If-None-Match
headers, let you build a front end that is more friendly to consumers' data plans and mobile battery lives. To do so:
-
Identify the entities that need locking and add a version attribute.
HTML5 nicely supports
data-*
attributes, so store the version in the DOM (somewhere such as andata-etag
attribute). -
Identify the entries that would benefit from tracking the most recent updates. When fetching these resources, store the
Last-Modified
value in the DOM (data-last-modified
perhaps). -
When fetching resources, also embed
self
URIs in your DOM nodes (perhapsdata-uri
ordata-self
) so that it is easy to go back to the resource. -
Adjust
PUT
/PATCH
/DELETE
operations to useIf-Match
and also handle HTTP412 Precondition Failed
status codes. -
Adjust
GET
operations to useIf-None-Match
andIf-Modified-Since
and handle HTTP304 Not Modified
status codes.
By embedding ETag
elements and Last-Modified
values in your DOM (or perhaps elsewhere for a native mobile app), you can then reduce the consumption of data and battery power by not retrieving the same thing over and over. You can also avoid colliding with other clients and, instead, be alerted when you need to reconcile differences.
In this fashion, with just a little tweaking on your front end and some entity-level edits, the backend serves up time-sensitive details you can cash in on when building a customer-friendly client.