Skip to content

Commit

Permalink
Added Creating Query Model article to the tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
dgomezg committed Apr 19, 2024
1 parent 8a8b16f commit b67f83d
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@
import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
//tag::BikeStatusEntity[]
@Entity //<.>
//end::BikeStatusEntity[]
//tag::BikeStatusClass[]
public class BikeStatus {

@Id
//tag::Fields[]
//tag::PersistenceIdAnnotation[]
@Id //<.>
//end::PersistenceIdAnnotation[]
private String bikeId;
private String bikeType;
private String location;
Expand All @@ -16,41 +22,35 @@ public class BikeStatus {
public BikeStatus() {
}

//end::Fields[]
//tag::Constructor[]
public BikeStatus(String bikeId, String bikeType, String location) {
this.bikeId = bikeId;
this.bikeType = bikeType;
this.location = location;
this.status = RentalStatus.AVAILABLE;
}

//end::Constructor[]
//tag::Methods[]
//tag::Accessors[]
// Accessor methods
public String getBikeId() {
return bikeId;
}

public String getLocation() {
return location;
public String getBikeType() {
return bikeType;
}

public void returnedAt(String location) {
this.location = location;
this.status = RentalStatus.AVAILABLE;
this.renter = null;
public String getLocation() {
return location;
}

public String getRenter() {
return renter;
}

public void requestedBy(String renter) {
this.renter = renter;
this.status = RentalStatus.REQUESTED;
}

public void rentedBy(String renter) {
this.renter = renter;
this.status = RentalStatus.RENTED;
}

public RentalStatus getStatus() {
return status;
}
Expand All @@ -68,7 +68,25 @@ public String description() {
}
}

public String getBikeType() {
return bikeType;
//end::Accessors[]
//tag::Modifiers[]
public void returnedAt(String location) {
this.location = location;
this.status = RentalStatus.AVAILABLE;
this.renter = null;
}


public void requestedBy(String renter) {
this.renter = renter;
this.status = RentalStatus.REQUESTED;
}

public void rentedBy(String renter) {
this.renter = renter;
this.status = RentalStatus.RENTED;
}
//end:Modifiers[]
//end::Methods[]
}
//end::BikeStatusClass[]
4 changes: 2 additions & 2 deletions docs/_playbook/playbook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ asciidoc:
experimental: true
page-pagination: true
kroki-fetch-diagram: true
primary-site-manifest-url: https://library.axoniq.io/site-manifest.json
# primary-site-manifest-url: https://library.axoniq.io/site-manifest.json
extensions:
- asciidoctor-kroki
- '@asciidoctor/tabs'
Expand All @@ -39,4 +39,4 @@ runtime:

ui:
bundle:
url: https://github.com/AxonIQ/axoniq-library-ui/releases/download/v.0.1.4/ui-bundle.zip
url: https://github.com/AxonIQ/axoniq-library-ui/releases/download/v.0.1.8/ui-bundle.zip
3 changes: 2 additions & 1 deletion docs/tutorial/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
* xref:implement-create-bike.adoc[]
* xref:run-app-with-docker-compose.adoc[]
* xref:invoking-create-bike-endpoint.adoc[]
* xref::unit-testing-commands.adoc[]
* xref::unit-testing-commands.adoc[]
* xref:create-bike-status-projection.adoc[]
175 changes: 175 additions & 0 deletions docs/tutorial/modules/ROOT/pages/create-bike-status-projection.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
:navtitle: Creating the Query Model
:reftext: Creating the model to reply queries

= Creating the Query Model

In this tutorial step, we will implement the Query Model, a component whose primary goal is to receive and handle any request for information about our system. These requests, which only expect some information in return and whose processing does not imply making any changes in our system, are known as *Queries*.

In order to efficiently handle and process a query request, we will design our system to maintain a version (or a _view_) of the data that is updated and aligned with the format in which users can request information from the system. This component that aims to keep a copy of the data aligned with the structure of the expected query response is called the *Projection*.

To keep the projection up to date with the changes made by other components (the command handlers) in the system, **the Query Model component must receive the event messages that represent the notification of changes made by the command model** and modify the projection accordingly. This way, our query model will be ready to handle any query request to return this updated information view.

If we recall the main diagram of our application, it's now time to focus on the bottom half of the diagram: implementing the components needed to handle and respond to queries.

include::partial$messages-flow-diagram.adoc[]

== Creating the `BikeStatus` response message

If, as we have just stated, our projection component will focus on handling queries to request information from our system, the first thing we need to consider when designing the query model is the exact request we will handle and how we will return the information.

In this case, we will implement support in our application to return information about one or more bikes, including where the bike is, whether it is available or rented and who has rented it.

So, we will model all the information expected from these queries in the `BikeStatus` class. We will define this class in the `core-api`:

[source,java]
.core-api/src/main/java/io/axoniq/demo/bikerental/coreapi/rental/BikeStatus.java
----
include::example$core-api/src/main/java/io/axoniq/demo/bikerental/coreapi/rental/BikeStatus.java[tags=BikeStatusClass;Fields;!*]
----

This class defines the fields with the information we need to present in the query response message.

To model the status of the bike, we will define the following Java enum:

[source,java]
.core-api/src/main/java/io/axoniq/demo/bikerental/coreapi/rental/RentalStatus.java
----
include::example$core-api/src/main/java/io/axoniq/demo/bikerental/coreapi/rental/RentalStatus.java[]
----

Finally, after we have all the fields for the `BikeStatus` response message, it's convenient to add methods to retrieve the information from the class. So, we can add the accessor methods:

[source,java]
.core-api/src/main/java/io/axoniq/demo/bikerental/coreapi/rental/BikeStatus.java
----
include::example$core-api/src/main/java/io/axoniq/demo/bikerental/coreapi/rental/BikeStatus.java[tags=BikeStatusClass;Accessors;!*]
----


== Creating the `BikeStatus` projection

Now that we have modeled the information we want to expose in response to requests to check the status of a bike. We can now create the component to keep this information updated and ready to be returned when a query request is processed.

=== Creating the `BikeStatus` class and the Spring `JpaRepository`
We need to create a `BikeStatusProjection` class in the `...rental.query` package of our `rental` module:

[source,java]
.rental/src/main/java/io/axoniq/demo/bikerental/rental/query/BikeStatusProjection.java
----
@Component
public class BikeStatusProjection {
include::example$rental/src/main/java/io/axoniq/demo/bikerental/rental/query/BikeStatusProjection.java[tags=Repository;!*]
public BikeStatusProjection(BikeStatusRepostory repository) {
this.bikeStatusRepository = repository;
}
}
----
<.> We will use a Spring repository to persist the `BikeStatus` model, which will be updated with the latest state based on the changes represented by the events received from the command model.

We need to define the Spring JPA repository we will use in our projection:
[source,java]
----
include::example$rental/src/main/java/io/axoniq/demo/bikerental/rental/query/BikeStatusRepository.java[tags=InterfaceDefinition;!*]
----
<.> The `org.springframework.stereotype.Repository` annotation instructs Spring to generate a `Repository` component from this interface.
<.> The convention for Spring JPA repositories is to create an interface that extends from `JpaRepository<T, ID>` where `T` is the type of the persisted classes and `ID` is the type of the identifier field in `T`. In this case, `T` should be annotated with `@Entity` and the `ID` should be of the same type as the field annotated with `@Id` in `T`

[NOTE]
====
With Spring Data support, this is all we need to define to have a `Repository` implementation that supports the basic operations of storing, updating, altering, querying, and dropping `BikeStatus` instances in the DB.
You can learn more about Spring Data Repositories in the section dedicated to https://docs.spring.io/spring-data/jpa/reference/repositories/definition.html["Defining Repository Interfaces" from the Spring Data JPA Reference]
====

Finally, to make the repository work, we must modify our `BikeStatus` class to add the persistence annotations. Open the `BikeStatus` class from the `core-api` module and introduce the following changes:

[source,java]
.core-api/src/main/java/io/axoniq/demo/bikerental/coreapi/rental/BikeStatus.java
----
include::example$core-api/src/main/java/io/axoniq/demo/bikerental/coreapi/rental/BikeStatus.java[tags=BikeStatusEntity;BikeStatusClass;Fields;PersistenceIdAnnotation;!*]
----
<.> The `Entity` annotation marks this class as a persistent entity. This is the `T` in the Spring's `JpaRepository<T,ID>`

<.> This annotation instruct the persistent layer to consider `bikeId` as the Id for the persistent record. The type of the field annotated with `@Id` (in this case `String`) is the `ID` in the Spring's `JpaRepository<T,ID>`

With these changes we are ready to define the methods in our `BikeStatusProjection` that should handle the events that notify changes made by the command model and update and persist the `BikeStatus`.

=== Define the `BikeRegisteredEvent` handler.

To keep the list of our bikes in the query model up to date, we need to define a method that will be invoked whenever a new bike is registered in the system (the `BikeRegisteredEvent` represents that notification). We can do this by adding an `@EventHandler` method to our `BikeStatusProjection`:

[source,java]
.rental/src/main/java/io/axoniq/demo/bikerental/rental/query/BikeStatusProjection.java
----
include::example$rental/src/main/java/io/axoniq/demo/bikerental/rental/query/BikeStatusProjection.java[tags=ClassDefinition;BikeRegisteredEventHandler;!*]
----
<.> The `@EventHandler` annotation instructs Axon Framework to register this component as a subscriber to `BikeRegisteredEvent` and call this method for each one.
<.> By default, Axon Framework uses the first argument in the method definition to match the type of events received and passes the event as an argument to the method.
<.> Since `BikeRegisteredEvent` implies that a new bike has been created in the system, we need to create a new instance of `BikeStatus` to represent the state of this new bike.
<.> Finally, we will persist the `BikeStatus` using the `bikeStatusRepository`

== Handling the queries from the projection.

Our final task in defining our projection is to implement the support for handling queries and returning the current information we have.

We need to add a `@QueryHandler` method for each query we want to support. Since we already have the bike statuses persisted in the way we need to return the information, we only need to query the database and return that.

=== Implement a query to return all the bikes.

Let's start by implementing a method to return all the bikes (with their status) defined in our system. Add the following method to your `BikeStatusProjection` class:

[source,java]
.rental/src/main/java/io/axoniq/demo/bikerental/rental/query/BikeStatusProjection.java
----
include::example$rental/src/main/java/io/axoniq/demo/bikerental/rental/query/BikeStatusProjection.java[tags=ClassDefinition;findAllQueryHandler;!*]
----
<.> The `org.axonframework.queryhandling.QueryHandler` annotation instructs Axon Framework to register this method as a target to invoke for certain types of queries. In this case, we identify the queries by name (although the type of the query message could also identify them, but we will see an example of that later), and we declare the specific name of the query to be handled by this method with the `queryName` attribute.
<.> Since our query has no parameters (we want to retrieve the information for *all the bikes* in our system), our query handler method does not receive any parameters. It only needs to return the list of items we find in our DB.
<.> As we have the information already prepared and aligned with the response format (thanks to the `EventHandler`s), we only need to retrieve the information from the repository and return it.

In short, we have defined a query handler method that Axon Framework will call upon the reception of a query message to `findAll` the bikes in our system. And the method will simply retrieve the up-to-date information from the DB and return the `BikeStatus` for all the bikes.

=== Implementing support for other queries in our projection.

We may need to support different query requests for information about the bikes in our system. The same projection can be used to satisfy different queries.

For example, if we want to support queries to return all the available bikes, filtering by the type, or the `BikeStatus` for a specific bike by its `bikeId`, we can add the following two methods to our `BikeStatusProjection`:

[source,java]
.rental/src/main/java/io/axoniq/demo/bikerental/rental/query/BikeStatusProjection.java
----
include::example$rental/src/main/java/io/axoniq/demo/bikerental/rental/query/BikeStatusProjection.java[tags=ClassDefinition;QueryHandlers;!*]
----
<.> We define a new `QueryHandler` method for the `findAvailable` query.
<.> The query will filter by the type of the bike, so we need to add the `bikeType` argument to the method.
<.> We need to add a specific method to our `BikeStatusRepository` that implements the query to the DB. We will do that right after this. But, since we are using Spring Data, the name of the method should follow a specific pattern. (More on this in a few lines)
<.> We define another `QueryHandler` method for the `findOne` query.
<.> In this query, we only need to return one bike, and we need the `bikeId` as an argument to the method. In this case, we will return a single `BikeStatus` because we are returning a single element and not a collection.
<.> The default `findById` method provided by the Spring Data `JpaRepository` returns an `Optional<T>` when we look up an item based on its `id`. This is because the `id` we are looking for may not exist in our DB. So we add a fallback to return `null` in case there is no bike with the given `bikeId` in the DB.

One last thing we need to add is a method to our Spring Data `BikeStatusRepository` to support the specific method to filter all records from the DB all the records by `bikeType` and `status`. Fortunately, thanks to Spring Data we only need to define a method in the `BikeStatusRepository` interface following a specific naming pattern, and Spring Data will generate the implementation with the corresponding SQL query to the DB.

So, go to the `BikeStatusRepository` and add the following method:
[source,java]
----
include::example$rental/src/main/java/io/axoniq/demo/bikerental/rental/query/BikeStatusRepository.java[tags=InterfaceDefinition;]
----

[NOTE]
====
When we define a Spring Data JPA repository that extends `JpaRepository<T,ID>`, Spring Data generates for us the implementation of a basic set of methods to query the database. These generated methods cover the operations of creating, updating, querying and deleting registers from the database.
Sometimes we need to define additional queries to filter elements according to different criteria. For these types of queries, Spring Data allows us to simply define new methods in our interface and, if we follow a certain naming convention, Spring will be able to infer the query that needs to be executed against the database from the name of the method and its arguments.
This is sometimes called *Derived Queries* and you can learn how to add specific methods for different queries in the section dedicated to https://docs.spring.io/spring-data/commons/reference/repositories/query-methods-details.html#repositories.query-methods.query-creation[Query Creation from the Spring Data Reference guide]
====

Now, our `BikeStatusProjection` fully supports answering to queries to `findAll` bikes, `findAvailable` bikes of a certain type, and `findOne` specific bike given its `bikeId`.

In the next section we will extend our `RestController` to add endpoints for these queries and route the queries to the system using `Query` messages.



Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ These design goals may seem complicated to achieve. Still, if we rely on the cor

In summary, our rental application will have the following high-level architecture diagram for handling requests to register a new bike in our system (and, generally, to handle all types of requests.)

image::image$logic-diagram.png[Design diagram with the logical modules for rental application: An UI/API module contains the HTTP Controller that receives the HTTP POST request to register a new bike. The HTTP Controller sends a RegisterBikeCommand to the Command Model module, including a CommandHandler to maintain the Bike aggregates. After processing a RegisterBikeCommand, the CommandHandler sends a BikeRegisteredEvent to the Query Model module, which keeps a Projection with the data stored on a DB. The Projection on the QueryModel also receives and handles a GetBikesQuery that can be sent from another Controller in the UI/API module.]
include::partial$messages-flow-diagram.adoc[]

These could be separate modules, but for now, we are going to consider these just as logical components within the same project: We will define different packages in the same project (in our case, the `rental` module)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
image::image$logic-diagram.png[Design diagram with the logical modules for rental application: An UI/API module contains the HTTP Controller that receives the HTTP POST request to register a new bike. The HTTP Controller sends a RegisterBikeCommand to the Command Model module, including a CommandHandler to maintain the Bike aggregates. After processing a RegisterBikeCommand, the CommandHandler sends a BikeRegisteredEvent to the Query Model module, which keeps a Projection with the data stored on a DB. The Projection on the QueryModel also receives and handles a GetBikesQuery that can be sent from another Controller in the UI/API module.]
Loading

0 comments on commit b67f83d

Please sign in to comment.