Databases: Comparison, Choosing & Polyglot
jsonb: Indexes, Tradeoffs & Exercise
>>>@>jsonb_setYou are continuing the books API from the previous page. You already have three books in the database - a novel, a textbook, and a programming guide with nested specs and tags.
On this page you will add endpoints to search and update books using JSON operators. Each section adds one method to BookRepository.java and one endpoint to BookController.java.
By the end of this page your API will support:

> and >>Two operators extract values from JSON:
| Operator | Returns | Use for |
|---|---|---|
->> |
Plain text — Fantasy |
WHERE clauses and comparisons |
-> |
JSON value — "Fantasy" |
Navigating into nested objects |
Chain -> to go deeper, then ->> at the end to get plain text:

BookRepository.java// ->> extracts as plain text — safe to compare with =
public List<Book> findByAuthor(String author) {
return jdbcTemplate.query(
"SELECT * FROM books WHERE details->>'author' = ?",
this::mapRow, author);
}
public List<Book> findByGenre(String genre) {
return jdbcTemplate.query(
"SELECT * FROM books WHERE details->>'genre' = ?",
this::mapRow, genre);
}
// -> navigates into specs object, ->> extracts ram as text
public String findRam(Long id) {
return jdbcTemplate.queryForObject(
"SELECT details->'specs'->>'ram' FROM books WHERE id = ?",
String.class, id);
}
BookController.java@GetMapping(params = "author")
public List<Book> getByAuthor(@RequestParam String author) {
return bookRepository.findByAuthor(author);
}
@GetMapping(params = "genre")
public List<Book> getByGenre(@RequestParam String genre) {
return bookRepository.findByGenre(genre);
}
@GetMapping("/{id}/ram")
public ResponseEntity<String> getRam(@PathVariable Long id) {
return ResponseEntity.ok(bookRepository.findRam(id));
}
GET /api/books?author=Tolkien → The Hobbit
GET /api/books?genre=Fantasy → The Hobbit
GET /api/books/3/ram → 16GB
Use -> with a number to get an element from a JSON array (zero-based index):
details->'tags'->0 → "java" (first element)
details->'tags'->1 → "programming" (second element)
BookRepository.java// ->0 gets the first element of the tags array
public String findFirstTag(Long id) {
return jdbcTemplate.queryForObject(
"SELECT details->'tags'->>0 FROM books WHERE id = ?",
String.class, id);
}
BookController.java@GetMapping("/{id}/first-tag")
public ResponseEntity<String> getFirstTag(@PathVariable Long id) {
return ResponseEntity.ok(bookRepository.findFirstTag(id));
}
GET /api/books/3/first-tag → java
Try changing ->>0 to ->>1 — you will get programming instead.
@>@> checks if the left JSON contains the right JSON. The real power is matching multiple fields at once - something ->> cannot do.

->> |
@> |
|
|---|---|---|
| Fields | One at a time | Multiple at once |
| Example | ?author=Tolkien |
?author=Tolkien&genre=Fantasy |
BookRepository.java// Single field search using @>
public List<Book> findByGenreContainment(String genre) {
return jdbcTemplate.query(
"SELECT * FROM books WHERE details @> CAST(? AS jsonb)",
this::mapRow,
"{\"genre\": \"" + genre + "\"}");
}
// Multi-field search — the real power of @>
public List<Book> findByAuthorAndGenre(String author, String genre) {
return jdbcTemplate.query(
"SELECT * FROM books WHERE details @> CAST(? AS jsonb)",
this::mapRow,
"{\"author\": \"" + author + "\", \"genre\": \"" + genre + "\"}");
}
BookController.java@GetMapping(value = "/search", params = "genre")
public List<Book> searchByGenre(@RequestParam String genre) {
return bookRepository.findByGenreContainment(genre);
}
@GetMapping(value = "/search", params = {"author", "genre"})
public List<Book> searchByAuthorAndGenre(
@RequestParam String author,
@RequestParam String genre) {
return bookRepository.findByAuthorAndGenre(author, genre);
}
GET /api/books/search?genre=Fantasy
→ all Fantasy books
GET /api/books/search?author=Tolkien&genre=Fantasy
→ only books where BOTH match
GET /api/books/search?author=Tolkien&genre=Programming
→ [] — Tolkien has no Programming books
jsonb_setjsonb_set updates one field without replacing the whole JSON object:

Strings vs numbers: string values need inner quotes "\"Sci-Fi\"". Number values do not — just pass "350" directly.
BookRepository.javajava
// String field — needs inner quotes
public void updateGenre(Long id, String genre) {
jdbcTemplate.update(
"UPDATE books SET details = jsonb_set(details, '{genre}', ?::jsonb) WHERE id = ?",
"\"" + genre + "\"", id);
}
// Number field — no inner quotes needed
public void updatePages(Long id, String pages) {
jdbcTemplate.update(
"UPDATE books SET details = jsonb_set(details, '{pages}', ?::jsonb) WHERE id = ?",
pages, id);
}
// Nested field — path uses comma: {specs,ram}
public void updateRam(Long id, String ram) {
jdbcTemplate.update(
"UPDATE books SET details = jsonb_set(details, '{specs,ram}', ?::jsonb) WHERE id = ?",
"\"" + ram + "\"", id);
}
BookController.java@PatchMapping("/{id}/genre")
public ResponseEntity<Void> updateGenre(
@PathVariable Long id, @RequestParam String value) {
bookRepository.updateGenre(id, value);
return ResponseEntity.noContent().build();
}
@PatchMapping("/{id}/pages")
public ResponseEntity<Void> updatePages(
@PathVariable Long id, @RequestParam String value) {
bookRepository.updatePages(id, value);
return ResponseEntity.noContent().build();
}
@PatchMapping("/{id}/ram")
public ResponseEntity<Void> updateRam(
@PathVariable Long id, @RequestParam String value) {
bookRepository.updateRam(id, value);
return ResponseEntity.noContent().build();
}
PATCH /api/books/1/genre?value=Sci-Fi → 204 No Content
GET /api/books → only genre changed, author + pages untouched ✅
PATCH /api/books/1/pages?value=350 → 204 No Content
PATCH /api/books/3/ram?value=32GB → 204 No Content
Verify in the database:
docker exec -it my-postgres psql -U hyfuser -d jsonb_demo
SELECT title, details->>'genre' AS genre, details->>'pages' AS pages FROM books;
| Operator | What it does | Example |
|---|---|---|
->> |
Extract as plain text | details->>'author' = ? |
-> |
Navigate nested object | details->'specs'->>'ram' |
->0 |
Get array element | details->'tags'->>0 |
@> |
Contains — single or multi field | details @> CAST(? AS jsonb) |
jsonb_set |
Update one field only | jsonb_set(details, '{genre}', ?) |