Implementing Views
You can access a single Entity with its Entity key. You might want to retrieve multiple Entities, or retrieve them using an attribute other than the key. Akka Serverless 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:
In addition, this page describes:
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. |
The akkaserverless-python-sdk
GitHub repository includes an example of all views described on this page.
View from a Value Entity
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, use any name for the table.
This example assumes the following customer
state is defined in a customer_domain.proto
file:
syntax = "proto3";
package customer.domain;
message CustomerState {
string customer_id = 1;
string email = 2;
string name = 3;
Address address = 4;
}
message Address {
string street = 1;
string city = 2;
}
Define the View service descriptor
To get a view of multiple customers by their name, define the View as a service
in Protobuf. The descriptor defines:
-
How to update the View
-
The source of View data
-
A
table
attribute that can be any name. Use this name in the querySELECT
statement for the View. -
The query that returns customers by name
syntax = "proto3";
package customer.view;
import "customer_domain.proto";
import "akkaserverless/annotations.proto";
import "google/protobuf/any.proto";
service CustomerByName {
rpc UpdateCustomer(domain.CustomerState) returns (domain.CustomerState) { (1)
option (akkaserverless.method).eventing.in = { (2)
value_entity: "customers"
};
option (akkaserverless.method).view.update = { (3)
table: "customers"
};
}
rpc GetCustomers(ByNameRequest) returns (stream domain.CustomerState) { (4)
option (akkaserverless.method).view.query = { (5)
query: "SELECT * FROM customers WHERE name = :customer_name"
};
}
}
message ByNameRequest {
string customer_name = 1;
}
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. 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. |
If the query should return only one result,
When no result is found, the request fails with gRPC status code |
See Query syntax reference for examples of valid query syntax.
Register the View
In the View implementation, register the View with Akka Serverless. In addition to passing the service descriptor and a unique identifier, pass any descriptors that define state. In this example, the customer_domain.proto
descriptor defines the Value Entity state:
Invoke the addComponent
function to register the view with the service. For example:
View from Event Sourced Entity
Construct Event Sourced Entity Views from the events that the Entity emits. Build a state representation from the events and Query them. Using a Customer Registry service example, to create a View for querying customers by name:
The example assumes a customer_domain.proto
file that defines the events that
will update the View when a name changes:
syntax = "proto3";
package customer.domain;
message CustomerCreated {
CustomerState customer = 1;
}
message CustomerNameChanged {
string new_name = 1;
}
message CustomerAddressChanged {
Address new_address = 1;
}
Define the View descriptor
A view descriptor:
-
Defines update methods for events.
-
Provides the source of the View.
-
Enables transformation updates.
-
Specifies a
table
attribute used by Akka Serverless to store the View. Pick any name and use it in the QuerySELECT
statement for the View.
The following example customer_view.proto
file defines a View to consume the CustomerCreated
and CustomerNameChanged
events. It must ignore all other events.
syntax = "proto3";
package customer.view;
import "customer_domain.proto";
import "akkaserverless/annotations.proto";
import "google/protobuf/any.proto";
service CustomerByNameView {
rpc ProcessCustomerCreated(domain.CustomerCreated) returns (domain.CustomerState) { (1)
option (akkaserverless.method).eventing.in = {
event_sourced_entity: "customers" (2)
};
option (akkaserverless.method).view.update = {
table: "customers"
transform_updates: true (3)
};
}
rpc ProcessCustomerNameChanged(domain.CustomerNameChanged) returns (domain.CustomerState) { (4)
option (akkaserverless.method).eventing.in = {
event_sourced_entity: "customers" (5)
};
option (akkaserverless.method).view.update = {
table: "customers"
transform_updates: true (6)
};
}
rpc IgnoreOtherEvents(google.protobuf.Any) returns (domain.CustomerState) { (7)
option (akkaserverless.method).eventing.in = {
event_sourced_entity: "customers"
};
option (akkaserverless.method).view.update = {
table: "customers"
transform_updates: true
};
};
rpc GetCustomers(ByNameRequest) returns (stream domain.CustomerState) {
option (akkaserverless.method).view.query = {
query: "SELECT * FROM customers WHERE name = :customer_name"
};
}
}
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. |
4 | One method for each event. |
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. |
7 | Ignore events not relevant to this view. |
See Query syntax reference for more examples of valid query syntax.
View from a topic
The source of a View can be an eventing topic. You define it in the same way as described in View from Event Sourced Entity or View from a Value Entity, but leave out the eventing.in
annotation in the Protobuf.
syntax = "proto3";
package customer.view;
import "customer_domain.proto";
import "akkaserverless/annotations.proto";
import "google/protobuf/any.proto";
service CustomerByNameViewFromTopic {
rpc ProcessCustomerCreated(domain.CustomerCreated) returns (domain.CustomerState) {
option (akkaserverless.method).eventing.in = {
topic: "customers" (1)
};
option (akkaserverless.method).view.update = {
table: "customers"
transform_updates: true
};
}
rpc ProcessCustomerNameChanged(domain.CustomerNameChanged) returns (domain.CustomerState) {
option (akkaserverless.method).eventing.in = {
topic: "customers"
};
option (akkaserverless.method).view.update = {
table: "customers"
transform_updates: true
};
}
rpc IgnoreOtherEvents(google.protobuf.Any) returns (domain.CustomerState) {
option (akkaserverless.method).eventing.in = {
event_sourced_entity: "customers"
};
option (akkaserverless.method).view.update = {
table: "customers"
transform_updates: true
};
};
rpc GetCustomers(ByNameRequest) returns (stream domain.CustomerState) {
option (akkaserverless.method).view.query = {
query: "SELECT * FROM customers WHERE name = :customer_name"
};
}
}
1 | This is the only difference from View from Event Sourced Entity. |
How to transform results
To obtain different results than shown in the examples above, you can transform them:
Relational projection
Instead of using SELECT *
you can define the columns to use in the response message:
syntax = "proto3";
package customer.view;
import "customer_domain.proto";
import "akkaserverless/annotations.proto";
import "google/protobuf/any.proto";
message CustomerSummary {
string id = 1;
string name = 2;
}
service CustomerSummaryByName {
rpc GetCustomers(ByNameRequest) returns (stream CustomerSummary) {
option (akkaserverless.method).view.query = {
query: "SELECT customer_id AS id, name FROM customers WHERE name = :customer_name"
};
}
rpc UpdateCustomer(domain.CustomerState) returns (domain.CustomerState) {
option (akkaserverless.method).eventing.in = {
value_entity: "customers"
};
option (akkaserverless.method).view.update = {
table: "customers"
};
}
}
Similarly, you can include values from the request message in the response, such as :request_id
:
SELECT :request_id, customer_id as id, name FROM customers WHERE name = :customer_name
Response message including the result
Instead of streamed results, you can include the results in a repeated field in the response message:
message CustomersResponse {
repeated domain.CustomerState results = 1; (1)
}
service CustomersResponseByName {
rpc GetCustomers(ByNameRequest) returns (CustomersResponse) { (2)
option (akkaserverless.method).view.query = {
query: "SELECT * AS results FROM customers WHERE name = :customer_name" (3)
};
}
rpc UpdateCustomer(domain.CustomerState) returns (domain.CustomerState) {
option (akkaserverless.method).eventing.in = {
value_entity: "customers"
};
option (akkaserverless.method).view.update = {
table: "customers"
};
}
}
1 | The response message contains a repeated field. |
2 | The return type is not streamed . |
3 | The repeated field is referenced in the query with * AS results . |
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:
SELECT * FROM customers WHERE name = :customer_name
If the query is changed, Akka Serverless might need to add other indexes. For example, changing the above query to filter on the city
would mean that Akka Serverless needs to build a View with the index on the city
column.
SELECT * FROM customers WHERE address.city = :city
Such changes require you to define a new View. Akka Serverless will then rebuild it from the source event log or value changes.
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 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 differentviewId
when Register the View. Keep the oldregisterView
. -
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 syntax reference
Define View queries in a language that is similar to SQL. The following examples illustrate the syntax for a customers
entity, where the .proto
file defines the table
attribute as customers
. To retrieve:
-
All customers without any filtering conditions (no WHERE clause):
SELECT * FROM customers
-
Customers with a name matching the
customer_name
property of the request:SELECT * FROM customers WHERE name = :customer_name
-
Customers with matching
customer_name
ANDcity
properties of the request:SELECT * FROM customers WHERE name = :customer_name AND address.city = :city
-
Customers in a city matching a literal value:
SELECT * FROM customers WHERE address.city = 'New York'
Query filter predicates
Filter predicates include:
-
=
equals -
!=
not equals -
>
greater than -
>=
greater than or equals -
<
less than -
<=
less than or equals
The filter conditions can be combined with AND
/OR
.
SELECT * FROM customers WHERE
name = :customer_name AND address.city = 'New York' OR
name = :customer_name AND address.city = 'San Francisco'