Spring Data for Cassandra: A Complete Example

Amal Amine
6 min readSep 1, 2019

--

In this guide, we’ll set up a Cassandra cluster on minikube, and learn how to use it from a Spring Boot microservice using Spring Data.

We’ll learn how to:

  • Establish a connection to Cassandra from a Spring Boot microservice.
  • Creat keyspaces, user-defined types, entities, and repositories.
  • Create a controller that executes a simple insert query.
  • Test our controller using JUnit.

The Building Blocks

Cassandra

The Apache Cassandra NoSQL Database has always been a popular choice for heavy-load applications that require scalability, high availability and fault-tolerance without compromising performance. It carries the load for applications like Facebook, Netflix, and Reddit.

As a Columnar NoSQL Database, it is optimized for quick lookups in large datasets.

What’s a columnar database?

One that reads and writes columns of data rather than the rows. From the perspective of a user, column-based and relational databases appear very similar.

Spring Data

According to spring.io, Spring Data’s mission is to provide a familiar and consistent, Spring-based programming model for data access while still retaining the special traits of the underlying data store.

Why Spring Data?

The objective is to eliminate implementation differences for Spring developers when using different data access technologies, relational and non-relational.

Basically, it’s an umbrella project which contains many sub-projects that are specific to a given database, like Cassandra, MongoDB, Redis, and others.

Setting up a Cassandra cluster

Installing and starting minikube

We’ll setup a Cassandra cluster on minikube — A tool that runs a single-node Kubernetes cluster inside a Virtual Machine on your local machine.

For Mac users: install it with Homebrew

brew cask install minikube

For Linux users: install it with cURL

curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 \
&& sudo install minikube-linux-amd64 /usr/local/bin/minikube

For Windows users: download and use the minikube installer.

For further support with installation, refer to minikube’s getting started guide.

Once we’ve setup minikube, we can start it using the command:

minikube start

Deploying Cassandra

Now that we have Kubernetes running locally, we’ll use a helm chart to deploy the database into our Kubernetes cluster.

First, clone the Cassandra chart, then install it from the same directory you’ve cloned the repository into using the command:

helm install -n cassandra incubator/cassandra

Verify your installing by running:

kubectl get all

You should see the corresponding resources created: a stateful set, a service, and pods.

Testing the connection to Cassandra through cqlsh

Cassandra uses the Cassandra Query Language (CQL), which offers a model close to SQL. To interact with Cassandra through CQL, we’ll use a command line shell called cqlsh. It can be installed using pip:

pip install cqlsh

Once it’s installed, we can start cqlsh from any terminal using the command:

cqlsh

If the connection was successful, we’re all set to start interacting with our database from an application.

Connecting to Cassandra from a microservice

Create a new Spring Boot application using the spring initializer.

Setting up environment variables

In your application.propertiesfile, ensure you’ve included the following:

spring.data.cassandra.contact-points=127.0.0.1 (or your corresponding connection uri)
spring.data.cassandra.username=<username, if any>
spring.data.cassandra.password=<password, if any>
spring.data.cassandra.keyspace=default
spring.data.cassandra.port=9042
spring.data.cassandra.schema-action=NONE

schema-action=NONE indicates that we do not want our database to be created or recreated on startup. The rest of the attributes are used by Spring Data to connect to the correct Cassandra cluster.

You may want to override the default timeouts in case of slow network connections to your database, which could be the possible if you’re connecting to a remote Kubernetes platform:

spring.data.cassandra.connect-timeout-millis=10000ms
spring.data.cassandra.read-timeout-millis=10000ms
spring.data.cassandra.connect-timeout=10000ms
spring.data.cassandra.read-timeout=10000ms
spring.data.cassandra.pool.pool-timeout=10000ms

Connecting to Cassandra using Spring Data

We’ll create a Java class called CassandraConfig:

CassandraConfig.java

Note that for the contactPointsand keyspace variables, setting a placeholder is mandatory when you’re reading their values from application.properties.

The getAuthProvider function enables us to connect to a password protected Cassandra instance.

Creating keyspaces, entities, repositories, and user-defined types

Keyspaces

A keyspace is the outermost container for data in Cassandra. It has a name and attributes that define its behavior. While the most common scenario is to have one keyspace per application, you could choose to segment your data into multiple keyspaces.

For the sake of comprehensiveness, we’ll create multiple keyspaces, A and B, and learn how to manage them.

To do so, execute the following CQL statements in cqlsh:

CREATE KEYSPACE A     WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };CREATE KEYSPACE B   WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };

Next, we’ll create a package for each keyspace in our project. Spring Data will recognizes that the entities and repositories under each package will belong to the same keyspace.

User-defined types

Cassandra supports a rich set of data types. In addition to that, it also supports the creation of user-defined data types (UDTs).

Let’s define a data type called identifier, which has an idKey, idType, and value by running the following CQL statement in cqlsh:

CREATE TYPE identifier (idKey varchar, idType varchar, value varchar);

We need to create a class for our UDT in our project as well:

Identifier.java

Note the use of the @UserDefinedType annotation.

Entities

An entity is a Java class that is mapped to a Cassandra table. The class includes all of the table’s columns or attributes.

Let’s create two entities, one in each keyspace. We can create EntityA in keyspace A by running the following CQL statement in cqlsh:

use A;CREATE TABLE EntityA (
name varchar,
description varchar,
entityIdentifier frozen<identifier>,
PRIMARY KEY ( name )
);

Next, we’ll create a Java class called EntityA which maps to the entity we created in Cassandra. Ensure that the class EntityA.java is created inside the package for keyspace A.

EntityA.java

Note the use of the @Table, @Column, and @PrimaryKey annotations to indicate that we’re defining a database table, column, or primary key respectively.

We’ve also made use of our user-defined type in this class by using the following annotation:

@CassandraType(type = DataType.Name.UDT, userTypeName = "identifier")

Entity in keyspace B:

Similarly, we can create EntityB in keyspace Bby running the following CQL statement in cqlsh:

use B;CREATE TABLE EntityB (
name varchar,
description varchar,
PRIMARY KEY ( name )
);

We’ll also create Java class EntityB which is mapped to the entity in Cassandra.

EntityB.java

Repositories

After creating the entities which map to Cassandra tables, we need to create the corresponding repositories for them. A repository extends the CassandraRepository class, which allows us access to methods which execute CQL queries. For example, a repository will give you a set of CRUD methods like count, delete, save, andfindById.

For the sake of our example, we’ll only make only use of the methods inherited from CrudRepository. So all we have to do is define an interface named RepositoryA which extends CassandraRepository with the EntityA as the target Cassandra table, and String as the type of its primary key.

EntityARepository.java

Similarly, we’ll define an interface for RepositoryB.

EntityBRepository.java

Creating a controller

At this point we have a working connection to Cassandra, along with our entities, repositories, and user-defined types created. We’re ready to execute queries against our database.

Let’s create a simple controller which takes a name and a description, and creates a new entry in Cassandra.

entityBController.java

Notice that by invoking entityBRepository's save method, we’re executing a create query against our database.

Testing Cassandra with JUnit

Now that we have a working endpoint, let’s write some tests to ensure our connection works (and keeps working) as expected.

First, create a test.propertiesfile to override the connection attributes for Cassandra; we wouldn’t want to execute the tests against our actual database and/or keyspaces.

spring.data.cassandra.contact-points=127.0.0.1 (or your corresponding connection uri for the test database)
spring.data.cassandra.username=<username, if any>
spring.data.cassandra.password=<password, if any>

Testing CRUD methods

To test the save method we used works as expected, we need to do the following:

  1. Create an instance of EntityB with some sample data — this will be our expected EntityB value.
  2. Execute the controller’s handler function with the sample data.
  3. Query the database with the primary key of our sample data — this will be our actual EntityB value.
  4. Assert that the expected EntityB value matches the actual EntityB value.
EntityBCRUDTests.java

Testing failed connections and other exceptions

To tests that the endpoint acts as expected in case the save method failed for any reason, we need to do the following:

  1. Mock EntityBRepository and inject the mock into our controller.
  2. Mock the save method to throw an exception.
  3. Assert that the endpoint behaves as expected. In the case of our simple example, it’ll throw an exception.
EntityBCRUDFailedTests.java

Looks like we’re all done! 🎉

So, what’s next?

We setup the building blocks to interact with a Cassandra database from a microservice. We can now enrich the repositories to execute customized queries like findByDescription or findByIdentifier. We can also build in better error handling by throwing customized exceptions.

--

--