Implementing Views in Java or Scala

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:

The remainder of 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.

Creating a 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:

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

Java
src/main/proto/customer/domain/customer_domain.proto
syntax = "proto3";

package customer.domain;

option java_outer_classname = "CustomerDomain";

import "akkaserverless/annotations.proto";

option (akkaserverless.file).value_entity = { (1)
  name: "CustomerValueEntity"
  entity_type: "customers"
  state: "CustomerState"
};

message CustomerState {
  string customer_id = 1;
  string email = 2;
  string name = 3;
  Address address = 4;
}

message Address {
  string street = 1;
  string city = 2;
}
Scala
src/main/proto/customer/domain/customer_domain.proto
syntax = "proto3";

package customer.domain;

import "akkaserverless/annotations.proto";

option (akkaserverless.file).value_entity = { (1)
  name: "CustomerValueEntity"
  entity_type: "customers"
  state: "CustomerState"
};

message CustomerState {
  string customer_id = 1;
  string email = 2;
  string name = 3;
  Address address = 4;
}

message Address {
  string street = 1;
  string city = 2;
}
1 The (akkaserverless.file).value_entity option configures code generation to provide base classes and initial implementations for a Value Entity.

Define the View service descriptor

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

Java
src/main/proto/customer/view/customer_view.proto
syntax = "proto3";

package customer.view;

option java_outer_classname = "CustomerViewModel";

import "customer/domain/customer_domain.proto";
import "akkaserverless/annotations.proto";
import "google/protobuf/any.proto";

service CustomerByName {
  option (akkaserverless.service) = {
    type: SERVICE_TYPE_VIEW
  };

  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;
}
Scala
src/main/proto/customer/view/customer_view.proto
syntax = "proto3";

package customer.view;

import "customer/domain/customer_domain.proto";
import "akkaserverless/annotations.proto";
import "google/protobuf/any.proto";

service CustomerByName {
  option (akkaserverless.service) = {
    type: SERVICE_TYPE_VIEW
  };

  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 entity_type: "customers" property of the (akkaserverless.file).value_entity option in the customer_domain.proto file.
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.
In this sample we use the internal domain.CustomerState as the state of the view. This is convenient since it allows automatic updates of the view without any logic but has the draw back that it implicitly makes the domain.CustomerState type a part of the public service API. Transforming the state to another type than the incoming update to avoid this can be seen in Creating a View from an Event Sourced Entity.

If the query should only return one result, remove the stream from the return type:

Java
rpc GetCustomer(ByEmailRequest) returns (domain.CustomerState) { (1)
  option (akkaserverless.method).view.query = {
    query: "SELECT * FROM customers WHERE email = :email"
  };
}
Scala
rpc GetCustomer(ByEmailRequest) returns (domain.CustomerState) { (1)
  option (akkaserverless.method).view.query = {
    query: "SELECT * FROM customers WHERE email = :email"
  };
}
1 Without stream when expecting single result.

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.

Registering a View

Once you’ve defined a View, register it with AkkaServerless by invoking the AkkaServerlessFactory.withComponents method in the Main class.

Java
src/main/java/customer/Main.java
public static AkkaServerless createAkkaServerless() {
  return AkkaServerlessFactory.withComponents(
    return AkkaServerlessFactory.withComponents(
      CustomerValueEntity::new,
      CustomerByNameView::new);

}
Scala
src/main/scala/customer/Main.scala
def createAkkaServerless(): AkkaServerless = {
  // The AkkaServerlessFactory automatically registers any generated Actions, Views or Entities,
  // and is kept up-to-date with any changes in your protobuf definitions.
  // If you prefer, you may remove this and manually register these components in a
  // `AkkaServerless()` instance.
  AkkaServerlessFactory.withComponents(
    return AkkaServerlessFactory.withComponents(
      new CustomerValueEntity(_),
      new CustomerByNameView(_))
}

Creating a View from an Event Sourced Entity

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:

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

Java
src/main/proto/customer/domain/customer_domain.proto
syntax = "proto3";

package customer.domain;

option java_outer_classname = "CustomerDomain";

import "akkaserverless/annotations.proto";

option (akkaserverless.file).event_sourced_entity = { (1)
  name: "CustomerEntity"
  entity_type: "customers"
  state: "CustomerState"
  events: ["CustomerCreated", "CustomerNameChanged", "CustomerAddressChanged"]
};

message CustomerState {
  string customer_id = 1;
  string email = 2;
  string name = 3;
  Address address = 4;
}

message Address {
  string street = 1;
  string city = 2;
}
message CustomerCreated {
  CustomerState customer = 1;
}

message CustomerNameChanged {
  string new_name = 1;
}

message CustomerAddressChanged {
  Address new_address = 1;
}
Scala
src/main/proto/customer/domain/customer_domain.proto
syntax = "proto3";

package customer.domain;

import "akkaserverless/annotations.proto";

option (akkaserverless.file).event_sourced_entity = { (1)
  name: "CustomerEntity"
  entity_type: "customers"
  state: "CustomerState"
  events: ["CustomerCreated", "CustomerNameChanged", "CustomerAddressChanged"]
};

message CustomerState {
  string customer_id = 1;
  string email = 2;
  string name = 3;
  Address address = 4;
}

message Address {
  string street = 1;
  string city = 2;
}
message CustomerCreated {
  CustomerState customer = 1;
}

message CustomerNameChanged {
  string new_name = 1;
}

message CustomerAddressChanged {
  Address new_address = 1;
}

It also assumes a customer_api.proto that defines the state stored in the view and returned by queries:

Java
src/main/proto/customer/api/customer_api.proto
option java_outer_classname = "CustomerApi";

import "google/protobuf/empty.proto";
import "akkaserverless/annotations.proto";

message Customer {
  string customer_id = 1 [(akkaserverless.field).entity_key = true];
  string email = 2;
  string name = 3;
  Address address = 4;
}

message Address {
  string street = 1;
  string city = 2;
}
Scala
src/main/proto/customer/api/customer_api.proto
import "google/protobuf/empty.proto";
import "akkaserverless/annotations.proto";

message Customer {
  string customer_id = 1 [(akkaserverless.field).entity_key = true];
  string email = 2;
  string name = 3;
  Address address = 4;
}

message Address {
  string street = 1;
  string city = 2;
}

Define a View descriptor to consume events

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

Java
src/main/proto/customer/customer_view.proto
syntax = "proto3";

package customer.view;

option java_outer_classname = "CustomerViewModel";

import "customer/domain/customer_domain.proto";
import "customer/api/customer_api.proto";
import "akkaserverless/annotations.proto";
import "google/protobuf/any.proto";

message ByNameRequest {
  string customer_name = 1;
}

service CustomerByName {
  option (akkaserverless.service) = { (1)
    type: SERVICE_TYPE_VIEW
  };

  rpc ProcessCustomerCreated(domain.CustomerCreated) returns (api.Customer) { (2)
    option (akkaserverless.method).eventing.in = {
      event_sourced_entity: "customers" (3)
    };
    option (akkaserverless.method).view.update = {
      table: "customers"
      transform_updates: true (4)
    };
  }

  rpc ProcessCustomerNameChanged(domain.CustomerNameChanged) returns (api.Customer) { (2)
    option (akkaserverless.method).eventing.in = {
      event_sourced_entity: "customers" (5)
    };
    option (akkaserverless.method).view.update = {
      table: "customers"
      transform_updates: true (6)
    };
  }

  rpc ProcessCustomerAddressChanged(domain.CustomerAddressChanged) returns (api.Customer) {
    option (akkaserverless.method).eventing.in = {
      event_sourced_entity: "customers"
    };
    option (akkaserverless.method).view.update = {
      table: "customers"
      transform_updates: true
    };
  }

  rpc IgnoreOtherEvents(google.protobuf.Any) returns (api.Customer) {
    option (akkaserverless.method).eventing.in = {
      event_sourced_entity: "customers" (5)
     };
     option (akkaserverless.method).view.update = {
       table: "customers"
       transform_updates: true (6)
     };
  };

  rpc GetCustomers(ByNameRequest) returns (stream api.Customer) {
    option (akkaserverless.method).view.query = {
      query: "SELECT * FROM customers WHERE name = :customer_name"
    };
  }
}
1 The akkaserverless.service option configures code generation to provide base classes and an initial implementation for the class transforming events to updates of the state.
2 Define an update method for each event.
3 The source of the View is from the journal of the "customers" Event Sourced Entity. This identifier is defined in the entity_type: "customers"` property of the (akkaserverless.file).event_sourced_entity option in the customer_domain.proto file.
4 Enable transform_updates to build the View state from the events.
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.
Scala
src/main/proto/customer/customer_view.proto
syntax = "proto3";

package customer.view;

import "customer/domain/customer_domain.proto";
import "customer/api/customer_api.proto";
import "akkaserverless/annotations.proto";
import "google/protobuf/any.proto";

message ByNameRequest {
  string customer_name = 1;
}

service CustomerByName {
  option (akkaserverless.service) = { (1)
    type: SERVICE_TYPE_VIEW
  };

  rpc ProcessCustomerCreated(domain.CustomerCreated) returns (api.Customer) { (2)
    option (akkaserverless.method).eventing.in = {
      event_sourced_entity: "customers" (3)
    };
    option (akkaserverless.method).view.update = {
      table: "customers"
      transform_updates: true (4)
    };
  }

  rpc ProcessCustomerNameChanged(domain.CustomerNameChanged) returns (api.Customer) { (2)
    option (akkaserverless.method).eventing.in = {
      event_sourced_entity: "customers" (5)
    };
    option (akkaserverless.method).view.update = {
      table: "customers"
      transform_updates: true (6)
    };
  }

  rpc ProcessCustomerAddressChanged(domain.CustomerAddressChanged) returns (api.Customer) {
    option (akkaserverless.method).eventing.in = {
      event_sourced_entity: "customers"
    };
    option (akkaserverless.method).view.update = {
      table: "customers"
      transform_updates: true
    };
  }

  rpc IgnoreOtherEvents(google.protobuf.Any) returns (api.Customer) {
    option (akkaserverless.method).eventing.in = {
      event_sourced_entity: "customers" (5)
     };
     option (akkaserverless.method).view.update = {
       table: "customers"
       transform_updates: true (6)
     };
  };

  rpc GetCustomers(ByNameRequest) returns (stream api.Customer) {
    option (akkaserverless.method).view.query = {
      query: "SELECT * FROM customers WHERE name = :customer_name"
    };
  }
}

See Query syntax reference for more examples of valid query syntax.

Create a transformation class

Next, you need to define how to transforms events to state that can be used in the View. An Event Sourced entity can emit many types of events. If a View does not use all events, you need to ignore unneeded events as shown in the IgnoreOtherEvents update handler:

The code-generation will generate an implementation class with an initial empty implementation which we’ll discuss below.

View update handlers are implemented in the CustomerByNameView class as methods that override abstract methods from AbstractCustomerByNameView. The methods take the current view state as the first parameter and the event as the second parameter. They return an UpdateEffect, which describes next processing actions, such as updating the view state.

When adding or changing the rpc definitions, including name, parameter and return messages, in the .proto files the corresponding methods are regenerated in the abstract class (AbstractCustomerByNameView). This means that the compiler will assist you with such changes. The IDE can typically fill in missing method signatures and such.

Java
src/main/java/customer/view/CustomerByNameView.java
import com.akkaserverless.javasdk.view.ViewContext;
import com.google.protobuf.Any;
import customer.domain.CustomerDomain;
import customer.api.CustomerApi;

public class CustomerByNameView extends AbstractCustomerByNameView { (1)

  public CustomerByNameView(ViewContext context) {}

  @Override
  public CustomerApi.Customer emptyState() { (2)
    return null;
  }

  @Override (3)
  public UpdateEffect<CustomerApi.Customer> processCustomerCreated(
    CustomerApi.Customer state,
    CustomerDomain.CustomerCreated customerCreated) {
    if (state != null) {
      return effects().ignore(); // already created
    } else {
      return effects().updateState(convertToApi(customerCreated.getCustomer()));
    }
  }

  @Override (3)
  public UpdateEffect<CustomerApi.Customer> processCustomerNameChanged(
    CustomerApi.Customer state,
    CustomerDomain.CustomerNameChanged customerNameChanged) {
    return effects().updateState(
        state.toBuilder().setName(customerNameChanged.getNewName()).build());
  }

  @Override (3)
  public UpdateEffect<CustomerApi.Customer> processCustomerAddressChanged(
    CustomerApi.Customer state,
    CustomerDomain.CustomerAddressChanged customerAddressChanged) {
    return effects().updateState(
        state.toBuilder().setAddress(convertToApi(customerAddressChanged.getNewAddress())).build());
  }

  @Override
  public UpdateEffect<CustomerApi.Customer> ignoreOtherEvents(
    CustomerApi.Customer state,
    Any any) {
    return effects().ignore();
  }

  private CustomerApi.Customer convertToApi(CustomerDomain.CustomerState s) {
    CustomerApi.Address address = CustomerApi.Address.getDefaultInstance();
    if (s.hasAddress()) {
      address = convertToApi(s.getAddress());
    }
    return CustomerApi.Customer.newBuilder()
        .setCustomerId(s.getCustomerId())
        .setEmail(s.getEmail())
        .setName(s.getName())
        .setAddress(address)
        .build();
  }

  private CustomerApi.Address convertToApi(CustomerDomain.Address a) {
    return CustomerApi.Address.newBuilder()
        .setStreet(a.getStreet())
        .setCity(a.getCity())
        .build();
  }
}
1 Extends the generated AbstractCustomerByNameView, which extends View new tab.
2 Defines the initial, empty, state that is used before any updates.
3 One method for each event.
Scala
src/main/scala/customer/view/CustomerByNameView.scala
import com.akkaserverless.scalasdk.view.View.UpdateEffect
import com.akkaserverless.scalasdk.view.ViewContext
import com.google.protobuf.any.{Any => ScalaPbAny}
import customer.api
import customer.api.Customer
import customer.domain
import customer.domain.CustomerAddressChanged
import customer.domain.CustomerCreated
import customer.domain.CustomerNameChanged
import customer.domain.CustomerState

class CustomerByNameView(context: ViewContext) extends AbstractCustomerByNameView { (1)

  override def emptyState: Customer = Customer.defaultInstance (2)

  override def processCustomerCreated(
    state: Customer, customerCreated: CustomerCreated): UpdateEffect[Customer] = (3)
    if (state != emptyState) effects.ignore() // already created
    else effects.updateState(convertToApi(customerCreated.customer.get))

  override def processCustomerNameChanged(
    state: Customer, customerNameChanged: CustomerNameChanged): UpdateEffect[Customer] = (3)
    effects.updateState(state.copy(name = customerNameChanged.newName))

  override def processCustomerAddressChanged(
    state: Customer, customerAddressChanged: CustomerAddressChanged): UpdateEffect[Customer] = (3)
    effects.updateState(state.copy(address = customerAddressChanged.newAddress.map(convertToApi)))

  override def ignoreOtherEvents(
    state: Customer, any: ScalaPbAny): UpdateEffect[Customer] =
    effects.ignore()

  private def convertToApi(customer: CustomerState): Customer =
    Customer(
      customerId = customer.customerId,
      name = customer.name,
      email = customer.email,
      address = customer.address.map(convertToApi),
    )

  private def convertToApi(address: domain.Address): api.Address =
    api.Address(
      street = address.street,
      city = address.city
    )
}
1 Extends the generated AbstractCustomerByNameView, which extends View new tab.
2 Defines the initial, empty, state that is used before any updates.
3 One method for each event.
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.

Register the View

Register the View class with AkkaServerless:

Java
src/main/java/customer/Main.java
public static AkkaServerless createAkkaServerless() {
  return AkkaServerlessFactory.withComponents(
    CustomerEntity::new,
    CustomerByNameView::new);
}
Scala
src/main/scala/customer/Main.scala
def createAkkaServerless(): AkkaServerless = {
  AkkaServerlessFactory.withComponents(
    new CustomerEntity(_),
    new CustomerByNameView(_))
}

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 Creating a View from an Event Sourced Entity or Creating a View from a Value Entity, but leave out the eventing.in annotation in the Protobuf file.

Java
src/main/proto/customer/view/customer_view.proto
syntax = "proto3";

package customer.view;

option java_outer_classname = "CustomerViewModel";

import "customer/domain/customer_domain.proto";
import "customer/api/customer_api.proto";
import "akkaserverless/annotations.proto";
import "google/protobuf/any.proto";

service CustomerByNameFromTopic {
  rpc ProcessCustomerCreated(domain.CustomerCreated) returns (api.Customer) {
    option (akkaserverless.method).eventing.in = {
      topic: "customers" (1)
    };
    option (akkaserverless.method).view.update = {
      table: "customers"
      transform_updates: true
    };
  }

  rpc ProcessCustomerNameChanged(domain.CustomerNameChanged) returns (api.Customer) {
    option (akkaserverless.method).eventing.in = {
      topic: "customers"
    };
    option (akkaserverless.method).view.update = {
      table: "customers"
      transform_updates: true
    };
  }

  rpc ProcessCustomerAddressChanged(domain.CustomerAddressChanged) returns (api.Customer) {
    option (akkaserverless.method).eventing.in = {
      topic: "customers"
    };
    option (akkaserverless.method).view.update = {
      table: "customers"
      transform_updates: true
    };
  }

  rpc IgnoreOtherEvents(google.protobuf.Any) returns (api.Customer) {
    option (akkaserverless.method).eventing.in = {
      topic: "customers"
     };
     option (akkaserverless.method).view.update = {
       table: "customers"
       transform_updates: true
     };
  };

  rpc GetCustomers(ByNameRequest) returns (stream api.Customer) {
    option (akkaserverless.method).view.query = {
      query: "SELECT * FROM customers WHERE name = :customer_name"
    };
  }
}
1 This is the only difference from Creating a View from an Event Sourced Entity.
Scala
src/main/proto/customer/view/customer_view.proto
syntax = "proto3";

package customer.view;

import "customer/domain/customer_domain.proto";
import "customer/api/customer_api.proto";
import "akkaserverless/annotations.proto";
import "google/protobuf/any.proto";

service CustomerByNameFromTopic {
  rpc ProcessCustomerCreated(domain.CustomerCreated) returns (api.Customer) {
    option (akkaserverless.method).eventing.in = {
      topic: "customers" (1)
    };
    option (akkaserverless.method).view.update = {
      table: "customers"
      transform_updates: true
    };
  }

  rpc ProcessCustomerNameChanged(domain.CustomerNameChanged) returns (api.Customer) {
    option (akkaserverless.method).eventing.in = {
      topic: "customers"
    };
    option (akkaserverless.method).view.update = {
      table: "customers"
      transform_updates: true
    };
  }

  rpc ProcessCustomerAddressChanged(domain.CustomerAddressChanged) returns (api.Customer) {
    option (akkaserverless.method).eventing.in = {
      topic: "customers"
    };
    option (akkaserverless.method).view.update = {
      table: "customers"
      transform_updates: true
    };
  }

  rpc IgnoreOtherEvents(google.protobuf.Any) returns (api.Customer) {
    option (akkaserverless.method).eventing.in = {
      topic: "customers"
     };
     option (akkaserverless.method).view.update = {
       table: "customers"
       transform_updates: true
     };
  };

  rpc GetCustomers(ByNameRequest) returns (stream api.Customer) {
    option (akkaserverless.method).view.query = {
      query: "SELECT * FROM customers WHERE name = :customer_name"
    };
  }
}
1 This is the only difference from Creating a View from an Event Sourced Entity.

How to transform results

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

REVIEWERS: it would be nice to have use cases describing why they might want to use these different transformation techniques. And does this information apply to all views, regardless of whether they were created from entities or topics?

Relational projection

Instead of using SELECT * you can define what columns that will be used in the response message:

Java
message CustomerSummary {
  string id = 1;
  string name = 2;
}

service CustomerSummaryByName {
  option (akkaserverless.service) = {
    type: SERVICE_TYPE_VIEW
  };

  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"
    };
  }
}
Scala
message CustomerSummary {
  string id = 1;
  string name = 2;
}

service CustomerSummaryByName {
  option (akkaserverless.service) = {
    type: SERVICE_TYPE_VIEW
  };

  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"
    };
  }
}

In a similar way, you can include values from the request message in the response, for example :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:

Java
message CustomersResponse {
  repeated domain.CustomerState results = 1; (1)
}

service CustomersResponseByName {
  option (akkaserverless.service) = {
    type: SERVICE_TYPE_VIEW
  };

  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"
    };
  }
}
Scala
message CustomersResponse {
  repeated domain.CustomerState results = 1; (1)
}

service CustomersResponseByName {
  option (akkaserverless.service) = {
    type: SERVICE_TYPE_VIEW
  };

  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:

  1. Define the new View, and keep the old View intact. A new View is defined by a new service in Protobuf. The viewId is the same as the service name, i.e. it will be a different viewId than the old View. Keep the old register of the old service in Main.

  2. Deploy the new View, and let it rebuild. Verify that the new query works as expected. The old View can still be used.

  3. Remove the old View definition and rename the new service to the old name if the public API is compatible, but keep the new viewId by defining it as shown below.

  4. Deploy the second change.

This is how to define a custom viewId:

Java
src/main/java/customer/Main.java
public static AkkaServerless createAkkaServerless() {
  AkkaServerless akkaServerless = new AkkaServerless();
  akkaServerless.register(CustomerByNameViewProvider.of(CustomerByNameView::new)
      .withViewId("CustomerByNameV2"));
  akkaServerless.register(CustomerEntityProvider.of(CustomerEntity::new));
  return akkaServerless;
}
Scala
src/main/scala/customer/Main.scala
def createAkkaServerless(): AkkaServerless =
  AkkaServerless()
    .register(
      CustomerByNameViewProvider(new CustomerByNameView(_))
        .withViewId("CustomerByNameV2"))
    .register(
      CustomerEntityProvider(new CustomerEntity(_))
    )

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. 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 message:

    SELECT * FROM customers WHERE name = :customer_name
  • Customers matching the customer_name AND city properties of the request message:

    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'

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.

SELECT * FROM customers WHERE
  name = :customer_name AND address.city = 'New York' OR
  name = :customer_name AND address.city = 'San Francisco'