Customer Registry with Views in Java/Protobuf
Create a customer registry that includes queries in Java, package it into a container, and run it on Kalix.
In this sample you will learn:
-
How to add additional functionality, allowing customers to be queried by name and email.
-
How to package the customer registry into a container.
-
How to deploy and run the customer registry on Kalix.
Before you begin
-
If you’re new to Kalix, create an account so you can try out Kalix for free.
-
You’ll need to install the Kalix CLI to deploy from a terminal window.
-
You’ll also need
-
Java 11 or higher
If you want to bypass writing code and jump straight to the deployment:
|
Start from the Customer Registry Entity
Start by downloading the Customer Registry sample source code using the Kalix CLI:
kalix quickstart download customer-registry-java
Define the CustomerByEmail View
-
In your project, create a directory for your views protobuf files,
src/main/proto/customer/view
.- Linux or macOS
-
mkdir -p ./src/main/proto/customer/view
- Windows 10+
-
mkdir src/main/proto/customer/view
-
Create a
customer_view.proto
file and save it in thesrc/main/proto/customer/view
directory.src/main/proto/customer/view/customer_view.protosyntax = "proto3"; (1) package customer.view; (2) option java_outer_classname = "CustomerViewModel"; (3) import "customer/domain/customer_domain.proto"; (4) import "kalix/annotations.proto";
1 The protobuf syntax version, proto3
.2 The package name, customer.view
.3 The Java outer classname, CustomerViewModel
. Messages defined in this file will be generated as inner classes, for exampleCustomerViewModel.ByEmailRequest
.4 Import the proto files for your domain model customer/domain/customer_domain.proto
and Kalix annotationskalix/annotations.proto
. -
Add the service endpoint
src/main/proto/customer/view/customer_view.protoservice CustomerByEmail { (1) option (kalix.codegen) = { (2) view: {} }; rpc UpdateCustomer(domain.CustomerState) returns (domain.CustomerState) { (3) option (kalix.method).eventing.in = { (4) value_entity: "customers" }; option (kalix.method).view.update = { (5) table: "customers_by_email" }; } rpc GetCustomer(ByEmailRequest) returns (domain.CustomerState) { (6) option (kalix.method).view.query = { query: "SELECT * FROM customers_by_email WHERE email = :email" (7) }; } } message ByEmailRequest { string email = 1; }
1 The Protobuf service
for the View.2 The option that the Maven plugin will use to generate the CustomerByEmail
View.3 The UpdateCustomer
method defines how Kalix will update the view.4 The source of the View is the "customers"
Value Entity. This identifier is defined in thetype_id: "customers"
property of the(kalix.codegen).value_entity
option in thecustomer_api.proto
file.5 The (kalix.method).view.update
annotation defines that this method is used for updating the View. You must define thetable
attribute for the table to be used in the query. Pick any name and use it in the querySELECT
statement.6 The GetCustomers
method defines the query to retrieve a customer by email.7 The (kalix.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 drawback that it implicitly makes thedomain.CustomerState
type a part of the public service API. Transforming the state to another type than the incoming update will be illustrated in theCustomerByName
example. -
Run
mvn compile
from the project root directory to generate source classes from the Protobuf definitions.mvn compile
This will result in a compilation error in the
Main
class. That is expected because you added a new component. Fix the compilation error by addingCustomerByEmailView::new
as the second parameter toKalixFactory.withComponents
insrc/main/java/customer/Main.java
.
Package and deploy your service
To build and publish the container image and then deploy the service, follow these steps:
-
If you haven’t done so yet, sign in to your Kalix account. If this is your first time using Kalix, this will let you register an account, create your first project, and set this project as the default.
kalix auth login
-
Use the
deploy
target to build the container image, publish it to the container registry as configured in thepom.xml
file, and use the targetkalix:deploy
to automatically deploy the service to Kalix:mvn deploy kalix:deploy
If you time stamp your image. For example, <dockerTag>${project.version}-${build.timestamp}</dockerTag>
you must always run both targets in one pass, i.e.mvn deploy kalix:deploy
. You cannot runmvn deploy
first and thenmvn kalix:deploy
because they will have different timestamps and thus different `dockerTag`s. This makes it impossible to reference the image in the repository from the second target. -
You can verify the status of the deployed service using:
kalix service list
Invoke your service
Once the service has started successfully, you can start a proxy locally to access the service:
kalix service proxy <service name> --grpcui
The --grpcui
option also starts and opens a gRPC web UI for exploring and invoking the service (available at http://127.0.0.1:8080/ui/).
Or you can use command line gRPC or HTTP clients, such as grpcurl
or curl
, to invoke the service through the proxy at localhost:8080
, using plaintext connections.
A customer can be created using the Create
method on CustomerService
, in the gRPC web UI, or with grpcurl
:
grpcurl \ -d '{ "customer_id": "abc123", "email": "someone@example.com", "name": "Someone", "address": { "street": "123 Some Street", "city": "Somewhere" } }' \ --plaintext localhost:8080 \ customer.api.CustomerService/Create
The GetCustomer
method can be used to retrieve this customer, in the gRPC web UI, or with grpcurl
:
grpcurl \ -d '{"customer_id": "abc123"}' \ --plaintext localhost:8080 \ customer.api.CustomerService/GetCustomer
The customer can also be found using the GetCustomer
method on the CustomerByEmail
view service, in the gRPC web UI, or with grpcurl
:
grpcurl \ -d '{"email": "someone@example.com"}' \ --plaintext localhost:8080 \ customer.view.CustomerByEmail/GetCustomer
You can expose the service to the internet. A generated hostname will be returned from the expose command:
kalix service expose <service name>
Define the CustomerByName View
-
In the same
src/main/proto/customer/view/customer_view.proto
file add another View for finding customers by name.Add the service endpoint
src/main/proto/customer/view/customer_view.protoservice CustomerByName { option (kalix.codegen) = { view: {} }; rpc UpdateCustomer(domain.CustomerState) returns (CustomerViewState) { (1) option (kalix.method).eventing.in = { value_entity: "customers" }; option (kalix.method).view.update = { table: "customers_by_name" }; } rpc GetCustomers(ByNameRequest) returns (stream CustomerViewState) { (2) option (kalix.method).view.query = { query: "SELECT * FROM customers_by_name WHERE name = :customer_name" }; } } message CustomerViewState { string customer_id = 1; string email = 2; string name = 3; } message ByNameRequest { string customer_name = 1; }
1 The UpdateCustomer
method defines how Kalix will update the view. In this case use aCustomerViewState
that is different from the incomingdomain.CustomerState
.2 The GetCustomers
method defines the query to retrieve customers by name. -
Run
mvn compile
from the project root directory to generate source classes from the Protobuf definitions.mvn compile
Again, this will result in a compilation error in the
Main
class. That is expected because you added a new component. Fix the compilation error by addingCustomerByNameView::new
as the third parameter toKalixFactory.withComponents
insrc/main/java/customer/Main.java
.
Implement UpdateCustomer
-
Implement the
emptyState
andupdateCustomer
method inCustomerByEmailView
:src/main/java/customer/view/CustomerByEmailView.javapublic class CustomerByNameView extends AbstractCustomerByNameView { public CustomerByNameView(ViewContext context) { } @Override public CustomerViewModel.CustomerViewState emptyState() { (1) return CustomerViewModel.CustomerViewState.getDefaultInstance(); } @Override public UpdateEffect<CustomerViewModel.CustomerViewState> updateCustomer( CustomerViewModel.CustomerViewState state, CustomerDomain.CustomerState customerState) { CustomerViewModel.CustomerViewState newViewState = (2) CustomerViewModel.CustomerViewState.newBuilder() .setCustomerId(customerState.getCustomerId()) .setEmail(customerState.getEmail()) .setName(customerState.getName()) .build(); return effects().updateState(newViewState); (3) } }
1 Empty state that will be used if no previous state has been stored for the View. 2 From the incoming CustomerDomain.CustomerState
that represents the change from the Value Entity create aCustomerViewModel.CustomerViewState
.3 Return effects().updateState
with the new state for the View.The state of the View is still per Entity. The CustomerDomain.CustomerState customerState
parameter represents the changed state of a specific Value Entity. Thestate
parameter is the existing state, if any, of the View for the Entity, i.e. the state that was previously returned viaeffects().updateState
. If no previous state has been stored theemptyState()
is used.
Deploy the updated service
-
Deploy the updated service by repeating the steps in Package and deploy your service.
Invoke the CustomerByName
-
Similar to the steps in Invoke your service.
-
Create several customers with same name.
-
Use the new
CustomerByName
instead ofCustomerByEmail
and then you should see multiple results fromCustomerByName/GetCustomers
for customers with the same name.
Next steps
-
You can read more about Views.