Using the Cassandra Client
Apache Cassandra® is a free and open-source, distributed, wide column store, NoSQL database management system designed to handle large amounts of data across many commodity servers, providing high availability with no single point of failure.
In this guide, we will see how you can get your REST services to use a Cassandra database.
This extension is developed by a third party and is part of the Quarkus Platform. |
Prerequisites
To complete this guide, you need:
-
Roughly 15 minutes
-
An IDE
-
JDK 17+ installed with
JAVA_HOME
configured appropriately -
Apache Maven 3.9.9
-
Optionally the Quarkus CLI if you want to use it
-
Optionally Mandrel or GraalVM installed and configured appropriately if you want to build a native executable (or Docker if you use a native container build)
-
A running Apache Cassandra, DataStax Enterprise (DSE) or DataStax Astra database; or alternatively, a fresh Docker installation.
Architecture
This quickstart guide shows how to build a REST application using the Cassandra Quarkus extension, which allows you to connect to an Apache Cassandra, DataStax Enterprise (DSE) or DataStax Astra database, using the DataStax Java driver.
This guide will also use the DataStax Object Mapper – a powerful Java-to-CQL mapping framework that greatly simplifies your application’s data access layer code by sparing you the hassle of writing your CQL queries by hand.
The application built in this quickstart guide is quite simple: the user can add elements in a list using a form, and the items list is updated. All the information between the browser and the server is formatted as JSON, and the elements are stored in the Cassandra database.
Solution
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
The solution is located in the quickstart directory of the Cassandra Quarkus extension GitHub repository.
Creating a Blank Maven Project
First, create a new Maven project and copy the pom.xml
file that is present in the quickstart
directory.
The pom.xml
is importing all the Quarkus extensions and dependencies you need.
Creating the Data Model and Data Access Objects
In this example, we will create an application to manage a list of fruits.
First, let’s create our data model – represented by the Fruit
class – as follows:
@Entity
@PropertyStrategy(mutable = false)
public class Fruit {
@PartitionKey
private final String name;
private final String description;
public Fruit(String name, String description) {
this.name = name;
this.description = description;
}
// getters, hashCode, equals, toString methods omitted for brevity
}
As stated above, we are using the DataStax Object Mapper. In other words, we are not going to write our CQL queries manually; instead, we will annotate our data model with a few annotations, and the mapper will generate proper CQL queries underneath.
This is why the Fruit
class is annotated with @Entity
: this annotation marks it as an entity
class that is mapped to a Cassandra table. Its instances are meant to be automatically persisted
into, and retrieved from, the Cassandra database. Here, the table name will be inferred from the
class name: fruit
.
Also, the name
field represents a Cassandra partition key, and so we are annotating it with
@PartitionKey
– another annotation from the Object Mapper library.
Entity classes are normally required to have a default no-arg constructor, unless they
are annotated with @PropertyStrategy(mutable = false) , which is the case here.
|
The next step is to create a DAO (Data Access Object) interface that will manage instances of
Fruit
entities:
@Dao
public interface FruitDao {
@Update
void update(Fruit fruit);
@Select
PagingIterable<Fruit> findAll();
}
This interface exposes operations that will be used in our REST service. Again, the annotation
@Dao
comes from the DataStax Object Mapper, which will also automatically generate an
implementation of this interface for you.
Note also the special return type of the findAll
method,
PagingIterable
:
it’s the base type of result sets returned by the driver.
Finally, let’s create a Mapper interface:
@Mapper
public interface FruitMapper {
@DaoFactory
FruitDao fruitDao();
}
The @Mapper
annotation is yet another annotation recognized by the DataStax Object Mapper. A
mapper is responsible for constructing DAO instances – in this case, out mapper is constructing
an instance of our only DAO, FruitDao
.
Think of the mapper interface as a factory for DAO beans. If you intend to construct and inject a
specific DAO bean in your own code, then you first must add a @DaoFactory
method for it in a
@Mapper
interface.
@DaoFactory method names are irrelevant.
|
@DaoFactory
methods should return beans of the following types:
-
Any
@Dao
-annotated interface, e.g.FruitDao
; -
A
CompletionStage
of any@Dao
-annotated interface, e.g.CompletionStage<FruitDao>
. -
A
Uni
of any@Dao
-annotated interface, e.g.Uni<FruitDao>
.
Uni is a type from the Mutiny library, which is the reactive programming library used by
Quarkus. This will be explained in more detail in the "Reactive Programming" section below.
|
Generating the DAO and mapper implementations
As you probably guessed already, we are not going to implement the interfaces above. Instead, the Object Mapper will generate such implementations for us.
The Object Mapper is composed of 2 pieces:
-
A (compile-time) annotation processor that scans the classpath for classes annotated with
@Mapper
,@Dao
or@Entity
, and generates code and CQL queries for them; and -
A runtime module that contains the logic to execute the generated queries.
Therefore, enabling the Object Mapper requires two steps:
-
Declare the
cassandra-quarkus-mapper-processor
annotation processor. With Maven, this is done by modifying the compiler plugin configuration in the project’spom.xml
file as follows:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>com.datastax.oss.quarkus</groupId>
<artifactId>cassandra-quarkus-mapper-processor</artifactId>
<version>${cassandra-quarkus.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
With Gradle, this is done by adding the following line to the build.gradle
file:
annotationProcessor "com.datastax.oss.quarkus:cassandra-quarkus-mapper-processor:${cassandra-quarkus.version}"
Verify that you are enabling the right annotation processor! The Cassandra driver ships
with its Object Mapper annotation processor, called java-driver-mapper-processor . But the
Cassandra Quarkus extension also ships with its own annotation processor:
cassandra-quarkus-mapper-processor , which has more capabilities than the driver’s. This annotation
processor is the only one suitable for use in a Quarkus application, so check that this is the one
in use. Also, never use both annotation processors together.
|
-
Declare the
java-driver-mapper-runtime
dependency in compile scope in the project’spom.xml
file as follows:
<dependency>
<groupId>com.datastax.oss</groupId>
<artifactId>java-driver-mapper-runtime</artifactId>
</dependency>
Although this module is called "runtime", it must be declared in compile scope. |
If your project is correctly set up, you should now be able to compile it without errors, and you
should see the generated code in the target/generated-sources/annotations
directory (if you are
using Maven). It’s not required to get familiar with the generated code though, as it is mostly
internal machinery to interact with the database.
Creating a service & JSON REST endpoint
Now let’s create a FruitService
that will be the business layer of our application and store/load
the fruits from the Cassandra database.
@ApplicationScoped
public class FruitService {
@Inject FruitDao dao;
public void save(Fruit fruit) {
dao.update(fruit);
}
public List<Fruit> getAll() {
return dao.findAll().all();
}
}
Note how the service is being injected a FruitDao
instance. This DAO instance is injected
automatically, thanks to the generated implementations.
The Cassandra Quarkus extension allows you to inject any of the following beans in your own components:
-
All
@Mapper
-annotated interfaces in your project. -
You can also inject a
CompletionStage
orUni
of any@Mapper
-annotated interface. -
Any bean returned by a
@DaoFactory
method (see above for possible bean types). -
The
QuarkusCqlSession
bean: this application-scoped, singleton bean is your main entry point to the Cassandra client; it is a specialized Cassandra driver session instance with a few methods tailored especially for Quarkus. Read its javadocs carefully! -
You can also inject
CompletionStage<QuarkusCqlSession>
orUni<QuarkusCqlSession>
.
In our example, both FruitMapper
and FruitDao
could be injected anywhere. We chose to inject
FruitDao
in FruitService
.
The last missing piece is the REST API that will expose GET and POST methods:
@Path("/fruits")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class FruitResource {
@Inject FruitService fruitService;
@GET
public List<FruitDto> getAll() {
return fruitService.getAll().stream().map(this::convertToDto).collect(Collectors.toList());
}
@POST
public void add(FruitDto fruit) {
fruitService.save(convertFromDto(fruit));
}
private FruitDto convertToDto(Fruit fruit) {
return new FruitDto(fruit.getName(), fruit.getDescription());
}
private Fruit convertFromDto(FruitDto fruitDto) {
return new Fruit(fruitDto.getName(), fruitDto.getDescription());
}
}
Notice how FruitResource
is being injected a FruitService
instance automatically.
It is generally not recommended using the same entity object between the REST API and the data
access layer. These layers should indeed be decoupled and use distinct APIs in order to allow each
API to evolve independently of the other. This is the reason why our REST API is using a different
object: the FruitDto
class – the word DTO stands for "Data Transfer Object". This DTO object will
be automatically converted to and from JSON in HTTP messages:
public class FruitDto {
private String name;
private String description;
public FruitDto() {}
public FruitDto(String name, String description) {
this.name = name;
this.description = description;
}
// getters and setters omitted for brevity
}
The translation to and from JSON is done automatically by the Quarkus REST (formerly RESTEasy Reactive) extension, which is included in this guide’s pom.xml file. If you want to add it manually to your application, add the below snippet to your application’s pom.xml file:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
DTO classes used by the JSON serialization layer are required to have a default no-arg constructor. |
The conversion from DTO to JSON is handled automatically for us, but we still must convert from
Fruit
to FruitDto
and vice versa. This must be done manually, which is why we have two
conversion methods declared in FruitResource
: convertToDto
and convertFromDto
.
In our example, Fruit and FruitDto are very similar, so you might wonder why not use
Fruit everywhere. In real life cases though, it’s not uncommon to see DTOs and entities having
very different structures.
|
Connecting to the Cassandra Database
Connecting to Apache Cassandra or DataStax Enterprise (DSE)
The main properties to configure are: contact-points
, to access the Cassandra database;
local-datacenter
, which is required by the driver; and – optionally – the keyspace to bind to.
A sample configuration should look like this:
quarkus.cassandra.contact-points={cassandra_ip}:9042
quarkus.cassandra.local-datacenter={dc_name}
quarkus.cassandra.keyspace={keyspace}
In this example, we are using a single instance running on localhost, and the keyspace containing
our data is k1
:
quarkus.cassandra.contact-points=127.0.0.1:9042
quarkus.cassandra.local-datacenter=datacenter1
quarkus.cassandra.keyspace=k1
If your cluster requires plain text authentication, you must also provide two more settings:
username
and password
.
quarkus.cassandra.auth.username=john
quarkus.cassandra.auth.password=s3cr3t
Connecting to a DataStax Astra Cloud Database
When connecting to DataStax Astra, instead of providing a contact point and a datacenter, you should provide a so-called secure connect bundle, which should point to a valid path to an Astra secure connect bundle file. You can download your secure connect bundle from the Astra web console.
You will also need to provide a username and password, since authentication is always required on Astra clusters.
A sample configuration for DataStax Astra should look like this:
quarkus.cassandra.cloud.secure-connect-bundle=/path/to/secure-connect-bundle.zip
quarkus.cassandra.auth.username=john
quarkus.cassandra.auth.password=s3cr3t
quarkus.cassandra.keyspace=k1
Advanced Driver Configuration
You can configure other Java driver settings using application.conf
or application.json
files.
They need to be located in the classpath of your application. All settings will be passed
automatically to the underlying driver configuration mechanism. Settings defined in
application.properties
with the quarkus.cassandra
prefix will have priority over settings
defined in application.conf
or application.json
.
To see the full list of settings, please refer to the driver settings reference.
Running a Local Cassandra Database
By default, the Cassandra client is configured to access a local Cassandra database on port 9042 (the default Cassandra port).
Make sure that the setting quarkus.cassandra.local-datacenter matches the datacenter of
your Cassandra cluster.
|
If you don’t know the name of your local datacenter, this value can be found by running the
following CQL query: SELECT data_center FROM system.local .
|
If you want to use Docker to run a Cassandra database, you can use the following command to launch one in the background:
docker run --name local-cassandra-instance -p 9042:9042 -d cassandra
Next you need to create the keyspace and table that will be used by your application. If you are using Docker, run the following commands:
docker exec -it local-cassandra-instance cqlsh -e "CREATE KEYSPACE IF NOT EXISTS k1 WITH replication = {'class':'SimpleStrategy', 'replication_factor':1}"
docker exec -it local-cassandra-instance cqlsh -e "CREATE TABLE IF NOT EXISTS k1.fruit(name text PRIMARY KEY, description text)"
You can also use the CQLSH utility to interactively interrogate your database:
docker exec -it local-cassandra-instance cqlsh
Testing the REST API
In the project root directory:
-
Run
mvn clean package
and thenjava -jar ./target/cassandra-quarkus-quickstart-*-runner.jar
to start the application; -
Or better yet, run the application in dev mode:
mvn clean quarkus:dev
.
Now you can use curl commands to interact with the underlying REST API.
To create a fruit:
curl --header "Content-Type: application/json" \
--request POST \
--data '{"name":"apple","description":"red and tasty"}' \
http://localhost:8080/fruits
To retrieve fruits:
curl -X GET http://localhost:8080/fruits
Creating a Frontend
Now let’s add a simple web page to interact with our FruitResource
.
Quarkus automatically serves static resources located under the META-INF/resources
directory. In
the src/main/resources/META-INF/resources
directory, add a fruits.html
file with the contents
from this file in it.
You can now interact with your REST service:
-
If you haven’t done yet, start your application with
mvn clean quarkus:dev
; -
Point your browser to
http://localhost:8080/fruits.html
; -
Add new fruits to the list via the form.
Reactive Programming with the Cassandra Client
The
QuarkusCqlSession
interface gives you access to a series of reactive methods that integrate seamlessly with Quarkus
and its reactive framework, Mutiny.
If you are not familiar with Mutiny, please check Mutiny - an intuitive reactive programming library. |
Let’s rewrite our application using reactive programming with Mutiny.
First, let’s declare another DAO interface that works in a reactive way:
@Dao
public interface ReactiveFruitDao {
@Update
Uni<Void> updateAsync(Fruit fruit);
@Select
MutinyMappedReactiveResultSet<Fruit> findAll();
}
Note the usage of MutinyMappedReactiveResultSet
- it is a specialized Mutiny
type converted from
the original Publisher
returned by the driver, which also exposes a few extra methods, e.g. to
obtain the query execution info. If you don’t need anything in that interface, you can also simply
declare your method to return Multi
: Multi<Fruit> findAll()
,
Similarly, the method updateAsync
returns a Uni
- it is automatically converted from the
original result set returned by the driver.
The Cassandra driver uses the Reactive Streams Publisher API for reactive calls. The Quarkus
framework however uses Mutiny. Because of that, the CqlQuarkusSession interface transparently
converts the Publisher instances returned by the driver into the reactive type Multi .
CqlQuarkusSession is also capable of converting a Publisher into a Uni – in this case, the
publisher is expected to emit at most one row, then complete. This is suitable for write queries
(they return no rows), or for read queries guaranteed to return one row at most (count queries, for
example).
|
Next, we need to adapt the FruitMapper
to construct a ReactiveFruitDao
instance:
@Mapper
public interface FruitMapper {
// the existing method omitted
@DaoFactory
ReactiveFruitDao reactiveFruitDao();
}
Now, we can create a ReactiveFruitService
that leverages our reactive DAO:
@ApplicationScoped
public class ReactiveFruitService {
@Inject ReactiveFruitDao fruitDao;
public Uni<Void> add(Fruit fruit) {
return fruitDao.update(fruit);
}
public Multi<Fruit> getAll() {
return fruitDao.findAll();
}
}
Finally, we can create a ReactiveFruitResource
:
@Path("/reactive-fruits")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ReactiveFruitResource {
@Inject ReactiveFruitService service;
@GET
public Multi<FruitDto> getAll() {
return service.getAll().map(this::convertToDto);
}
@POST
public Uni<Void> add(FruitDto fruitDto) {
return service.add(convertFromDto(fruitDto));
}
private FruitDto convertToDto(Fruit fruit) {
return new FruitDto(fruit.getName(), fruit.getDescription());
}
private Fruit convertFromDto(FruitDto fruitDto) {
return new Fruit(fruitDto.getName(), fruitDto.getDescription());
}
}
The above resource is exposing a new endpoint, reactive-fruits
. Its capabilities are identical to
the ones that we created before with FruitResource
, but everything is handled in a reactive
fashion, without any blocking operation.
The getAll() method above returns Multi , and the add() method returns Uni . These types
are the same Mutiny types that we met before; they are automatically recognized by the Quarkus
reactive REST API, so we don’t need to convert them into JSON ourselves.
|
Quarkus REST natively supports the Mutiny reactive types e.g. Uni
and Multi
.
This dependency is already included in this guide’s pom.xml, but if you are starting a new project from scratch, make sure to include it.
Testing the Reactive REST API
Run the application in dev mode as explained above, then you can use curl commands to interact with the underlying REST API.
To create a fruit using the reactive REST endpoint:
curl --header "Content-Type: application/json" \
--request POST \
--data '{"name":"banana","description":"yellow and sweet"}' \
http://localhost:8080/reactive-fruits
To retrieve fruits with the reactive REST endpoint:
curl -X GET http://localhost:8080/reactive-fruits
Creating a Reactive Frontend
Now let’s add a simple web page to interact with our ReactiveFruitResource
. In the
src/main/resources/META-INF/resources
directory, add a reactive-fruits.html
file with the
contents from this file in it.
You can now interact with your reactive REST service:
-
If you haven’t done yet, start your application with
mvn clean quarkus:dev
; -
Point your browser to
http://localhost:8080/reactive-fruits.html
; -
Add new fruits to the list via the form.
Health Checks
If you are using the Quarkus SmallRye Health extension, then the Cassandra client will automatically add a readiness health check to validate the connection to the Cassandra cluster. This extension is already included in this guide’s pom.xml, but if you need to include it manually in your application, add the following:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-health</artifactId>
</dependency>
When health checks are available, you can access the /health/ready
endpoint of your application
and have information about the connection validation status.
Running in dev mode with mvn clean quarkus:dev
, if you point your browser to
http://localhost:8080/health/ready you should see an output similar to the following one:
{
"status": "UP",
"checks": [
{
"name": "DataStax Apache Cassandra Driver health check",
"status": "UP",
"data": {
"cqlVersion": "3.4.4",
"releaseVersion": "3.11.7",
"clusterName": "Test Cluster",
"datacenter": "datacenter1",
"numberOfNodes": 1
}
}
]
}
If you need health checks globally enabled in your application, but don’t want to activate
Cassandra health checks, you can disable Cassandra health checks by setting the
quarkus.cassandra.health.enabled property to false in your application.properties .
|
Metrics
The Cassandra Quarkus client can provide metrics about the Cassandra session and about individual Cassandra nodes. It supports both Micrometer and MicroProfile.
The first step to enable metrics is to add a few additional dependencies depending on the metrics framework you plan to use.
Enabling Metrics with Micrometer
Micrometer is the recommended metrics framework in Quarkus applications.
To enable Micrometer metrics in your application, you need to add the following to your pom.xml.
<dependency>
<groupId>com.datastax.oss</groupId>
<artifactId>java-driver-metrics-micrometer</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
</dependency>
This guide uses Micrometer, so the above dependencies are already included in this guide’s pom.xml.
Enabling Metrics with MicroProfile Metrics
Remove any dependency to Micrometer from your pom.xml, then add the following ones instead:
<dependency>
<groupId>com.datastax.oss</groupId>
<artifactId>java-driver-metrics-microprofile</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-metrics</artifactId>
</dependency>
Enabling Cassandra Metrics
Even when metrics are enabled in your application, the Cassandra client will not report any metrics,
unless you opt in for this feature. So your next step is to enable Cassandra metrics in your
application.properties
file.
quarkus.cassandra.metrics.enabled=true
That’s it!
The final (and optional) step is to customize which specific Cassandra metrics you would like the Cassandra client to track. Several metrics can be tracked; if you skip this step, a default set of useful metrics will be automatically tracked.
For the full list of available metric names, please refer to the
driver
settings reference page; search for the advanced.metrics section.
Also, Cassandra driver metrics are covered in detail in the
driver manual.
|
If you do wish to customize which metrics to track, you should use the following properties:
-
quarkus.cassandra.metrics.session.enabled
should contain the session-level metrics to enable (metrics that are global to the session). -
quarkus.cassandra.metrics.node.enabled
should contain the node-level metrics to enable (metrics for which each node contacted by the Cassandra client gets its own metric value).
Both properties accept a comma-separated list of valid metric names.
For example, let’s assume that you wish to enable the following three Cassandra metrics:
-
Session-level:
session.connected-nodes
andsession.bytes-sent
; -
Node-level:
node.pool.open-connections
.
Then you should add the following settings to your application.properties
:
quarkus.cassandra.metrics.enabled=true
quarkus.cassandra.metrics.session.enabled=connected-nodes,bytes-sent
quarkus.cassandra.metrics.node.enabled=pool.open-connections
This guide’s application.properties
file has already many metrics enabled; you can use its metrics
list as a good starting point for exposing useful Cassandra metrics in your application.
When metrics are properly enabled, metric reports for all enabled metrics are available at the
/metrics
REST endpoint of your application.
Running in dev mode with mvn clean quarkus:dev
, if you point your browser to
http://localhost:8080/metrics
you should see a list of metrics; search for metrics whose names
contain cassandra
.
For Cassandra metrics to show up, the Cassandra client needs to be initialized and connected; if you are using lazy initialization (see below), you won’t see any Cassandra metrics until your application actually connects and hits the database for the first time. |
Running in native mode
If you installed GraalVM, you can build a native image using:
mvn clean package -Dnative
Beware that native compilation can take a significant amount of time! Once the compilation is done, you can run the native executable as follows:
./target/cassandra-quarkus-quickstart-*-runner
You can then point your browser to http://localhost:8080/fruits.html
and use your application.
Choosing between eager and lazy initialization
As explained above, this extension allows you to inject many types of beans:
-
A simple bean like
QuarkusCqlSession
orFruitDao
; -
The asynchronous version of that bean, for example
CompletionStage<QuarkusCqlSession>
or `CompletionStage<FruitDao>; -
The reactive version of that bean, for example
Uni<QuarkusCqlSession>
orUni<FruitDao>
.
The most straightforward approach is obviously to inject the bean directly. This should work just
fine for most applications. However, the QuarkusCqlSession
bean, and all DAO beans that depend on
it, might take some time to initialize before they can be used for the first time, and this process
is blocking.
Fortunately, it is possible to control when the initialization should happen: the
quarkus.cassandra.init.eager-init
parameter determines if the QuarkusCqlSession
bean should be
initialized on its first access (lazy) or when the application is starting (eager). The default
value of this parameter is false
, meaning the init process is lazy: the QuarkusCqlSession
bean
will be initialized lazily on its first access – for example, when there is a first REST request
that needs to interact with the Cassandra database.
Using lazy initialization speeds up your application startup time, and avoids startup failures if
the Cassandra database is not available. However, it could also prove dangerous if your code is
fully non-blocking, for example if it uses reactive routes.
Indeed, the lazy initialization could accidentally happen on a thread that is not allowed
to block, such as a Vert.x event loop thread. Therefore, setting quarkus.cassandra.init.eager-init
to false
and injecting QuarkusCqlSession
should be avoided in these contexts.
If you want to use Vert.x (or any other non-blocking framework) and keep the lazy initialization
behavior, you should instead inject only a CompletionStage
or a Uni
of the desired bean. When
injecting these beans, the initialization process will be triggered lazily, but it will happen in
the background, in a non-blocking way, leveraging the Vert.x event loop. This way you don’t risk
blocking the Vert.x thread.
Alternatively, you can set quarkus.cassandra.init.eager-init
to true: in this case the session
bean and all DAO beans will be initialized eagerly during application startup, on the Quarkus main
thread. This would eliminate any risk of blocking a Vert.x thread, at the cost of making your
startup time (much) longer.