Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 105 additions & 73 deletions docs/src/modules/java/pages/views.adoc
Original file line number Diff line number Diff line change
@@ -1,38 +1,56 @@
= Implementing Views

include::ROOT:partial$include.adoc[]
include::partial$views.adoc[]

You can access a single https://developer.lightbend.com/docs/akka-serverless/reference/glossary.html#entity[Entity] with its https://developer.lightbend.com/docs/akka-serverless/reference/glossary.html#entity_key[Entity key]. You might want to retrieve multiple Entities, or retrieve them using an attribute other than the key. Akka Serverless https://developer.lightbend.com/docs/akka-serverless/reference/glossary.html#view[Views] allow you achieve this. By creating multiple Views, you can optimize for query performance against each one.

Views can be defined from any of the following:

* xref:value-entity[Value Entities state changes]
* xref:event-sourced-entity[Event Sourced Entity events]
* xref:topic-view[Messages received from subscribing to topics on a broker]

The remainder of this page describes:

* <<_how_to_transform_results>>
* <<#changing>>
* <<#query>>

IMPORTANT: Be aware that Views are not updated immediately when Entity state changes. Akka Serverless does update Views as quickly as possible, but it is not instant and can take up to a few seconds for the changes to become visible in the query results. View updates might also take more time during failure scenarios than during normal operation.

[#value-entity]
== Creating a View from a Value Entity

Using an example of a customer registry, you can define a `Customer` Value Entity in Protobuf as:
Consider an example of a Customer Registry service with a `customer` Value Entity. When `customer` state changes, the entire state is emitted as a value change. Value changes update any associated Views. To create a View that lists customers by their name:

* <<_define_the_view_service_descriptor>> for a service that selects customers by name and associates a table name with the View. The table is created and used by Akka Serverless to store the View.

* xref:register-view[Register the View].

This example assumes the following `customer` state is defined in a `customer_domain.proto` file:

[source,proto,indent=0]
----
include::java:example$java-customer-registry/src/main/proto/customer/customer_domain.proto[tag=domain]
----

When `Customer` state changes, the entire state will be emitted as a value change, which will update any associated Views.
=== Define the View service descriptor

ifdef::todo[TODO: the description below reveals that we are storing Value Entities in tables for the user under the covers. This has not yet been introduced elsewhere and should be.]

To get a View of multiple customers by their name, define the view service in Protobuf:
To get a View of multiple customers by their name, define the View as a `service` in Protobuf:

[source,proto,indent=0]
----
include::java:example$java-customer-registry/src/main/proto/customer/customer_view.proto[tag=service]
----

<1> The `UpdateCustomer` method defines how the View is updated.
<1> The `UpdateCustomer` method defines how Akka Serverless will update the view.
<2> The source of the View is the `"customers"` Value Entity. This identifier is defined in the `@ValueEntity(entityType = "customers")` annotation of the Value Entity.
<3> The `(akkaserverless.method).view.update` annotation defines that this method is used for updating the View. The `table` property must be defined and corresponds to the table used in the query. You can use any name for the table.
<4> The `GetCustomers` method defines the query to retrieve a stream of `Customer`.
<3> The `(akkaserverless.method).view.update` annotation defines that this method is used for updating the View. You must define the `table` attribute for the table to be used in the query. Pick any name and use it in the query `SELECT` statement.
<4> The `GetCustomers` method defines the query to retrieve a stream of customers.
<5> The `(akkaserverless.method).view.query` annotation defines that this method is used as a query of the View.

ifdef::review[REVIEWERS: Who comes up with the table name, is it arbitrary and gets created just to store the view, or must it correspond to one of the Entity's data members? ]

If the query is supposed to return only one result, remove the `stream` from the return type:
If the query should only return one result, remove the `stream` from the return type:

[source,proto,indent=0]
----
Expand All @@ -41,60 +59,13 @@ include::java:example$java-customer-registry/src/main/proto/customer/customer_vi

<1> Without `stream` when expecting single result.

When no result is found, the request will fail with gRPC status code `NOT_FOUND`. A streamed call would complete with an empty stream when no result is found.


[#query]
== Querying a View

You define View queries in a language that is similar to SQL. The following examples illustrate the syntax.

To get all customers without any filtering conditions (no WHERE clause):
[source,proto,indent=0]
----
SELECT * FROM customers
----

To get customers with a name matching the `customer_name` property of the request message:
[source,proto,indent=0]
----
SELECT * FROM customers WHERE name = :customer_name
----

To get customers matching the `customer_name` AND `city` properties of the request message:
[source,proto,indent=0]
----
SELECT * FROM customers WHERE name = :customer_name AND address.city = :city
----

To get customers in a city matching a literal value:
[source,proto,indent=0]
----
SELECT * FROM customers WHERE address.city = 'New York'
----
When no result is found, the request fails with gRPC status code `NOT_FOUND`. A streamed call completes with an empty stream when no result is found.

You can use the following filter predicates to further refine results:

* `=` equals
* `!=` not equals
* `>` greater than
* `>=` greater than or equals
* `<` less than
* `\<=` less than or equals

You can combine filter conditions with the `AND` and `OR` operators.

[source,proto,indent=0]
----
SELECT * FROM customers WHERE
name = :customer_name AND address.city = 'New York' OR
name = :customer_name AND address.city = 'San Francisco'
----

[#register-view]
== Registering a View
=== Registering a View

Once you've defined a View, register it with the link:{attachmentsdir}/api/com/akkaserverless/javasdk/AkkaServerless.html[`AkkaServerless`] server, by invoking the `registerView` method. In addition to passing the service descriptor of the View, and a unique identifier of the View, you also need to pass any descriptors that you use for events, for example, the `domain.proto` descriptor.
Once you've defined a View, register it with link:{attachmentsdir}/api/com/akkaserverless/javasdk/AkkaServerless.html[`AkkaServerless`] by invoking the `registerView` method. In addition to passing the service descriptor of the View, and a unique identifier of the View, you also need to pass any descriptors that you use for state. In this example, that is the `customer_domain.proto` descriptor.

[source,java,indent=0]
----
Expand All @@ -104,17 +75,22 @@ include::java:example$java-customer-registry/src/main/java/customer/Main.java[ta
[#event-sourced-entity]
== Creating a View from an Event Sourced Entity

The previous example derived a View from a Value Entity and used state changes to update the View. In contrast, to create a View from an Event Sourced Entity, you use events that the Entity emits to build a state representation.
Create a View from an Event Sourced Entity by using events that the Entity emits to build a state representation. Using a Customer Registry service example, to create a View for querying customers by name:

=== Protobuf definition
. <<_define_a_view_descriptor_to_consume_events>>
. <<_create_a_transformation_class>>
. <<#es_register>>

Like the Value Entity example above, this View will provide a way to query customers. The Protobuf file defines the following events that will update the View:

The example assumes a `customer_domain.proto` file that defines the events that will update the View on name changes:

[source,proto,indent=0]
----
include::java:example$java-customer-registry/src/main/proto/customer/customer_domain.proto[tag=events]
----

=== Define a View descriptor to consume events

The following lines in the `.proto` file define a View to consume the `CustomerCreated` and `CustomerNameChanged` events:

[source,proto,indent=0]
Expand All @@ -124,14 +100,14 @@ include::java:example$java-customer-registry/src/main/proto/customer/customer_vi

<1> Define an update method for each event.
<2> The source of the View is from the journal of the `"customers"` Event Sourced Entity. This identifier is defined in the `@EventSourcedEntity(entityType = "customers")` annotation of the Event Sourced Entity.
<3> Enable `transform_updates` to be able to build the View state from the events.
<3> Enable `transform_updates` to build the View state from the events.
<4> One method for each event.
<5> Same `event_sourced_entity` for all update methods.
<5> The same `event_sourced_entity` for all update methods. Note the required `table` attribute. Use any name, which you will reference in the query `SELECT` statement.
<6> Enable `transform_updates` for all update methods.

The query definition works in the same way as described in the <<query>> section.
See <<#query>> for more examples of valid query syntax.

=== Update transformation class
=== Create a transformation class

Next, you need to define a Java class that transforms events to state that can be used in the View:

Expand All @@ -150,9 +126,10 @@ A second parameter can optionally be defined for the previous state. Its type co

The method may also take an link:{attachmentsdir}/api/com/akkaserverless/javasdk/view/UpdateHandlerContext.html[`UpdateHandlerContext`] parameter.

NOTE: Events from an Event Sourced Entity is the canonical use case for this kind of update transformation class, but it can also be used for Value Entities. For example, if the View representation is different from the Entity state.
NOTE: This type of update transformation is a natural fit for Events emitted by an Event Sourced Entity, but it can also be used for Value Entities. For example, if the View representation is different from the Entity state you might want to transform it before presenting the View to the client.

=== Registering
[#es_register]
=== Register the View

Register the View class with `AkkaServerless`:

Expand All @@ -161,6 +138,7 @@ Register the View class with `AkkaServerless`:
include::java:example$java-customer-registry/src/main/java/customer/Main.java[tag=register-with-class]
----

[#topic-view]
== Creating a View from a topic

The source of a View can be an eventing topic. You define it in the same way as shown in <<event-sourced-entity>> or <<value-entity>>, but leave out the `eventing.in` annotation in the Protobuf file.
Expand All @@ -172,7 +150,7 @@ include::java:example$java-customer-registry/src/main/proto/customer/customer_vi

<1> This is the only difference from <<event-sourced-entity>>.

== Transform result
== How to transform results

When creating a View, you can transform the results as a relational projection instead of using a `SELECT *` statement.

Expand Down Expand Up @@ -209,7 +187,7 @@ include::java:example$java-customer-registry/src/main/proto/customer/customer_vi

// anchor for error messages, do not remove.
[#changing]
== Changing a View
== How to modify a View

Akka Serverless creates indexes for the View based on the query. For example, the following query will result in a View with an index on the `name` column:

Expand All @@ -229,11 +207,65 @@ Such changes require you to define a new View. Akka Serverless will then rebuild

WARNING: Views from topics cannot be rebuilt from the source messages, because it's not possible to consume all events from the topic again. The new View will be built from new messages published to the topic.

Rebuilding a new View may take some time if there are many events that have to be processed. The recommended way when changing a View is a two-step deployment.
Rebuilding a new View may take some time if there are many events that have to be processed. The recommended way when changing a View is multi-step, with two deployments:


. Define the new View, and keep the old View intact. A new View is defined by a new `service` in Protobuf and different `viewId` when <<register-view>>. Keep the old `registerView`.
. Deploy the new View, and let it rebuild. Verify that the new query works as expected. The old View can still be used.
. Remove the old View definition and rename the `service` to the old name if the public API is compatible.
. Deploy the second change.

The View definitions are stored and validated when a new version is deployed. There will be an error message if the changes are not compatible.

[#query]
== Query syntax reference

Define View queries in a language that is similar to SQL. The following examples illustrate the syntax. To retrieve:

* All customers without any filtering conditions (no WHERE clause):
+
[source,proto,indent=0]
----
SELECT * FROM customers
----

* Customers with a name matching the `customer_name` property of the request message:
+
[source,proto,indent=0]
----
SELECT * FROM customers WHERE name = :customer_name
----

* Customers matching the `customer_name` AND `city` properties of the request message:
+
[source,proto,indent=0]
----
SELECT * FROM customers WHERE name = :customer_name AND address.city = :city
----

* Customers in a city matching a literal value:
+
[source,proto,indent=0]
----
SELECT * FROM customers WHERE address.city = 'New York'
----

=== Filter predicates

Use the following filter predicates to further refine results:

* `=` equals
* `!=` not equals
* `>` greater than
* `>=` greater than or equals
* `<` less than
* `\<=` less than or equals

Combine filter conditions with the `AND` and `OR` operators.

[source,proto,indent=0]
----
SELECT * FROM customers WHERE
name = :customer_name AND address.city = 'New York' OR
name = :customer_name AND address.city = 'San Francisco'
----
11 changes: 0 additions & 11 deletions docs/src/modules/java/partials/views.adoc

This file was deleted.