Building Read Models from Events
In this tutorial you will learn, how to aggregate events to persist them as read models, by building a book catalog. This catalog may be queried by interested readers to search for available books by ISBN, title, or author, thus focusing on the Q in CQRS.
Persisting Books within the Catalog
For building a persistent book catalog, the following requirements need to be met:
- Book copies need to be aggregated by ISBN, title, and author to enable search. This can
be derived from
BookPurchasedEvent
. - Book copy ids need to be stored to deduce the total number of copies per book. This can
be derived from
BookPurchasedEvent
, as well. - Book rentals and returns need to be reflected within a book's availability. This can
be derived from
BookLentEvent
andBookReturnedEvent
, respectively.
The following JPA entity needs to be created in src/main/java/com/example/cqrs
to persist
the required information:
package com.example.cqrs;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import java.util.*;
@Entity
@Table(name = "BOOK_CATALOG")
public class BookCatalogEntity {
@Id
private String isbn;
private String title;
private String author;
private Set<UUID> copies = new HashSet<>();
private Set<UUID> available = new HashSet<>();
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
@JsonIgnore
public Set<UUID> getCopies() {
return copies;
}
public Set<UUID> getAvailable() {
return available;
}
public int getTotal() {
return copies.size();
}
}
In order to store, update, and read BookCatalogEntity
instances, a Spring Data JPA
repository needs to be defined, as follows:
package com.example.cqrs;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import java.util.Optional;
import java.util.UUID;
public interface BookCatalogRepository extends CrudRepository<BookCatalogEntity, String> {
@Query(nativeQuery = true, value = "SELECT * FROM BOOK_CATALOG WHERE ARRAY_CONTAINS(copies, :id)")
Optional<BookCatalogEntity> findByCopiesContaining(UUID id);
Iterable<BookCatalogEntity> findAllByIsbnContainingIgnoreCaseAndTitleIsContainingIgnoreCaseAndAuthorContainingIgnoreCase(String isbn, String title, String author);
}
It defines two additional query methods:
- one for looking up a
BookCatalogEntity
for a given book copy id - one for searching the book catalog with any combination of ISBN, title, or author
Populating the Catalog
The BOOK_CATALOG
table needs to be filled driven by the events stored within the EventSourcingDB. Accordingly,
event handlers need to be defined, mapping the different domain events to BookCatalogEntity
. Create a
BookCatalogProjector
as follows:
This ensures, that:
BookPurchasedEvent
creates a newBookCatalogEntity
if necessary, populating its ISBN, title, and author, then updates the book copies and availability (lines 17-31).BookLentEvent
marks the book copy as currently not available (lines 33-40).BookReturnedEvent
marks the book copy as available, again (lines 42-49).
Querying the Catalog
With the event handlers in place, the BookCatalogRepository
can be used within a new REST controller to
expose the data to REST clients. For this, create the following BookCatalogController
:
package com.example.cqrs;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/books/catalog")
public class BookCatalogController {
private final BookCatalogRepository repository;
public BookCatalogController(BookCatalogRepository repository) {
this.repository = repository;
}
@GetMapping
public Iterable<BookCatalogEntity> fetch(
@RequestParam(required = false, defaultValue = "") String isbn,
@RequestParam(required = false, defaultValue = "") String title,
@RequestParam(required = false, defaultValue = "") String author
) {
return repository.findAllByIsbnContainingIgnoreCaseAndTitleIsContainingIgnoreCaseAndAuthorContainingIgnoreCase(isbn, title, author);
}
}
Testing the Application
Finally, after starting the EventSourcingDB and our application, you can query the book catalog, for instance matching the book title, using:
The output for a book with two copies, which is partially available, may look as follows:
[
{
"isbn": "978-0008471286",
"title": "Lord of the Rings",
"author": "JRR Tolkien",
"available": [
"fb9c7d71-9a5e-4664-8b75-73f4d04cac5e"
],
"total": 2
}
]
You have now successfully handled events, aggregated, and projected them to a SQL database to be able to query them via an additional REST API.