How to Use ETags for Optimistic Locking in ORDS

Stop REST clients from overwriting each other's changes with ETags and If-Match, and skip unchanged responses with 304.

Two clients read the same offer, both change it, and the second save silently overwrites the first. ETags prevent that. ORDS sends an ETag header with each resource, a fingerprint of its current state. A client that sends the ETag back in If-Match on a PUT gets 412 Precondition Failed if someone else changed the resource in between. If-None-Match on a GET saves bandwidth with 304 Not Modified.

Before You Start

You need ORDS installed and running against your database, and a schema to work in. The examples use NIMBUS, the sample schema of a fictional airline, which you can install with the scripts in the setup/nimbus folder of the Oracle Database 26ai code repository on GitHub. The schema comes from Oracle Database 26ai SQL and PL/SQL Book.

ORDS in the examples answers at https://localhost:8443/ords/, and NIMBUS is REST-enabled with the URL alias nimbus. Replace the host and port with your own ORDS address. The curl commands use -k because the test server has a self-signed certificate; leave it out when your certificate is trusted. JSON responses are formatted for reading; ORDS returns them on one line.

The examples use the offers table published with AutoREST in How to REST-Enable a Table with ORDS AutoREST, and the lounge module from How to Write a POST Handler That Inserts Rows in ORDS.

Syntax

GET  ...   -> response header  ETag: "value"
GET  ...   with  If-None-Match: "value"   -> 304 when unchanged
PUT  ...   with  If-Match: "value"        -> 412 when changed since

ords.define_template(..., p_etag_type => 'HASH');    -- default: hash of the response
ords.define_template(..., p_etag_type => 'QUERY',
                          p_etag_query => 'select ... where id = :id');

Read the ETag

Example:

curl -k -D - -o /dev/null https://localhost:8443/ords/nimbus/offers/1

Output (status and ETag):

HTTP/1.1 200 OK
ETag: "LYWBGhOJ14IIAFV6DhAhzw9o5purS6fON+ia/Zrpg0I+AP7haFrecLbpC9n+juftgf2tcK8kkhR9hoNM5M0emQ=="

Ask Only for Changes

Example:

curl -k -D - -o /dev/null -H 'If-None-Match: "ETAG_FROM_THE_GET"' \
  https://localhost:8443/ords/nimbus/offers/1

Output:

HTTP/1.1 304 Not Modified
ETag: "LYWBGhOJ14IIAFV6DhAhzw9o5purS6fON+ia/Zrpg0I+AP7haFrecLbpC9n+juftgf2tcK8kkhR9hoNM5M0emQ=="

The offer has not changed, so ORDS answers 304 with no body.

Update Only the Version You Read

A PUT with the current ETag in If-Match succeeds and returns a new ETag:

Output:

HTTP/1.1 200 OK
ETag: "adwPQMCn2BumCu5oLxb49O43DTqwsMbbOlQezn5wv1KqEuyOS+5IUFilLgmyH7/EoMxpdvZzb4ElkjuPJAWaQQ=="
{"offer_id":1,"airport_code":"DXB","title":"Day pass, Terminal 3 lounge","price_usd":62,"valid_until":"2026-12-31T00:00:00Z","links":[{"rel":"self","href":"https://localhost:8443/ords/nimbus/offers/1"},{"rel":"edit","href":"https://localhost:8443/ords/nimbus/offers/1"},{"rel":"describedby","href":"https://localhost:8443/ords/nimbus/metadata-catalog/offers/item"},{"rel":"collection","href":"https://localhost:8443/ords/nimbus/offers/"}]}

A second PUT that still sends the first ETag is refused, because the offer has changed since that version:

Output:

HTTP/1.1 412 Precondition Failed
Content-Type: application/problem+json
Content-Length: 204

{
    "code": "PredconditionFailed",
    "message": "Predcondition Failed",
    "type": "tag:oracle.com,2020:error/PredconditionFailed",
    "instance": "tag:oracle.com,2020:ecid/NYaYGyZwBz1STOGKrlP9jQ"
}

The spelling PredconditionFailed is ORDS's own. The client should read the offer again, apply its change to the new version, and retry.

ETags for REST Module Templates

Templates use a hash of the response by default. With p_etag_type set to QUERY, ORDS computes the ETag from a query of your own, for example over the columns that matter:

Example:

begin
  ords.define_template(p_module_name => 'lounge',
                       p_pattern     => 'offers/:id',
                       p_etag_type   => 'QUERY',
                       p_etag_query  => 'select ora_hash(title || price_usd || valid_until)
                                         from   lounge_offers where offer_id = :id');
  commit;
end;
/
select uri_template, etag_type, etag_query from user_ords_templates where uri_template = 'offers/:id';

Output:

PL/SQL procedure successfully completed.

URI_TEMPLATE    ETAG_TYPE    ETAG_QUERY
_______________ ____________ _____________________________________________________________________________________
offers/:id      QUERY        select ora_hash(title || price_usd || valid_until)
                                                                      from   lounge_offers where offer_id = :id

ORDS.DEFINE_TEMPLATE replaces the template, and in the test that removed its GET, PUT, and DELETE handlers: the endpoint answered 404 until they were defined again.

Example:

-- DEFINE_TEMPLATE replaced the template, so its handlers have to be defined again
begin
  ords.define_handler(p_module_name => 'lounge',
                      p_pattern     => 'offers/:id',
                      p_method      => 'GET',
                      p_source_type => ords.source_type_collection_item,
                      p_source      => 'select offer_id, airport_code, title, price_usd, valid_until
                                        from   lounge_offers
                                        where  offer_id = :id');
  commit;
end;
/

The template then returns an ETag, and a GET with that value in If-None-Match gets 304:

Output:

HTTP/1.1 200 OK
ETag: "CJZfWQdEJzntRLQj6mfmiK44eTw1fUYeqJI1gPn4C2eAAe9ujcKXes15PvT6EwliYqvGE0cZuzewVBXWXJILqg=="
HTTP/1.1 304 Not Modified

Things to Know

  • ETags are quoted strings: send them back exactly as received, quotes included.
  • A QUERY ETag is cheaper than hashing a large response, and can ignore columns that do not matter to clients.
  • If-Match is optional for clients; only those that send it are protected against lost updates.

Related Guides

Conclusion

ORDS ETags give REST clients optimistic locking and cheap revalidation: send If-Match on updates to get 412 instead of a lost update, and If-None-Match on reads to get 304 when nothing changed. For module templates, choose HASH or a QUERY of your own, and define handlers again after redefining a template.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE, author of four books on Oracle APEX, SQL and PL/SQL, and Oracle Forms, and a software developer building Oracle database applications since 2001.

guest

0 Comments
Oldest
Newest Most Voted