© 2008-2021 The original author(s).

Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.

Preface

The Spring Data for Apache Cassandra project applies core Spring concepts to the development of solutions using the Cassandra Columnar data store. A “template” is provided as a high-level abstraction for storing and querying documents. This project has noticeable similarities to the JDBC support in the core Spring Framework.

This document is the reference guide for Spring Data support for Cassandra. It explains Cassandra module concepts and semantics and the syntax for various stores namespaces.

This section provides a basic introduction to Spring, Spring Data, and the Cassandra database. The rest of the document refers only to Spring Data for Apache Cassandra features and assumes you are familiar with Cassandra as well as core Spring concepts.

1. Knowing Spring

Spring Data uses the Spring Framework’s core functionality, including:

While it is not important to know the Spring APIs, understanding the concepts behind them is important. At a minimum, the idea behind IoC should be familiar, no matter what IoC container you choose to use.

The core functionality of the Cassandra support can be used directly, with no need to invoke the IoC services of the Spring container. This is much like JdbcTemplate, which can be used 'standalone' without any other services of the Spring container. To use all the features of Spring Data for Apache Cassandra, such as the repository support, you must configure some parts of the library by using Spring.

To learn more about Spring, you can refer to the comprehensive documentation that explains the Spring Framework in detail. There are a lot of articles, blog entries, and books on Spring. See the Spring Framework home page for more information.

2. Knowing NoSQL and Cassandra

NoSQL stores have taken the storage world by storm. It is a vast domain with a plethora of solutions, terms, and patterns. (To make things worse, even the term itself has multiple meanings.) While some of the principles are common, it is crucial that you be familiar to some degree with the Cassandra Columnar NoSQL Datastore supported by Spring Data for Apache Cassandra. The best way to get acquainted with Cassandra is to read the documentation and follow the examples. It usually does not take more then 5-10 minutes to go through them, and, if you come from a RDBMS background, these exercises can often be an eye opener.

The starting point for learning about Cassandra is cassandra.apache.org. Also, here is a list of other useful resources:

3. Requirements

Spring Data for Apache Cassandra 2.x binaries require JDK level 8.0 and later and Spring Framework 5.3.10 and later.

It requires Cassandra 2.0 or later and Datastax driver 4.x.

4. Additional Help Resources

Learning a new framework is not always straight forward. In this section, we try to provide what we think is an easy-to-follow guide for starting with the Spring Data for Apache Cassandra module. However, if you encounter issues or you need advice, feel free to use one of the links below:

Community Forum

Spring Data on Stack Overflow is a tag for all Spring Data (not just Cassandra) users to share information and help each other. Note that registration is needed only for posting. The two key tags to search for related answers to this project are spring-data and spring-data-cassandra.

Professional Support

Professional, from-the-source support, with guaranteed response time, is available from Pivotal Sofware, Inc., the company behind Spring Data and Spring.

4.1. Following Development

For information on the Spring Data for Apache Cassandra source code repository, nightly builds, and snapshot artifacts see the Spring Data for Apache Cassandra home page. You can help make Spring Data best serve the needs of the Spring community by interacting with developers through the community on Stack Overflow. To follow developer activity, look for the mailing list information on the Spring Data for Apache Cassandra home page. If you encounter a bug or want to suggest an improvement, please create a ticket on the Spring Data issue tracker. To stay up-to-date with the latest news and announcements in the Spring ecosystem, subscribe to the Spring Community Portal. Finally, you can follow the Spring blog or the project team on Twitter (SpringData).

5. New & Noteworthy

This chapter summarizes changes and new features for each release.

5.1. What’s new in Spring Data for Apache Cassandra 3.2

  • Support for prepared statements using CassandraTemplate and repositories (enabled by default).

  • @Column and @Element can be used on constructor arguments.

  • Schema support for static columns through @Column(isStatic = …).

5.2. What’s new in Spring Data for Apache Cassandra 3.1

  • Reactive auditing enabled through @EnableReactiveCassandraAuditing. @EnableCassandraAuditing no longer registers ReactiveAuditingEntityCallback.

  • Reactive SpEL support in @Query query methods.

  • Configuration of the keyspace per Statement through CqlTemplate and QueryOptions.

  • Revised CqlOperations with new queryForStream(…) methods returning a Stream with transparent pagination.

  • DataClassRowMapper to map Cassandra results to data classes via constructor creation/bean properties.

5.3. What’s new in Spring Data for Apache Cassandra 3.0

5.4. What’s new in Spring Data for Apache Cassandra 2.2

  • Filter conditions for lightweight transaction update and delete (UPDATE … IF <condition>, DELETE … IF <condition>).

  • Optimistic Locking support.

  • Auditing via @EnableCassandraAuditing.

  • Lightweight transaction support via DeleteOptions using the Template API.

  • Support for derived Between queries.

  • Query derivation for DELETE queries.

  • Kotlin Coroutines support for ReactiveFluentCassandraOperations.

  • Idempotency support in @Query annotation.

  • Read-only properties annotated with @ReadOnlyProperty to exclude non-writable properties from entity-bound INSERT and UPDATE operations.

5.5. What’s new in Spring Data for Apache Cassandra 2.1

  • New annotations for @CountQuery and @ExistsQuery.

  • Template API extended with count(…) and exists(…) methods accepting Query.

  • Fluent API for CRUD operations.

  • Cassandra Mapped Tuple support via @Tuple.

  • Support for Cassandra time columns via LocalTime.

  • Support for map columns using User-defined/converted types.

  • Lifecycle Events.

  • Kotlin extensions for Template API.

  • Reactive Paging support through Mono<Slice<T>>.

5.6. What’s new in Spring Data for Apache Cassandra 2.0

  • Upgraded to Java 8.

  • Reactive Apache Cassandra support.

  • Merged spring-cql and spring-data-cassandra modules into a single module and re-packaged org.springframework.cql to org.springframework.data.cassandra.

  • Revised CqlTemplate, AsyncCqlTemplate, CassandraTemplate and AsyncCassandraTemplate implementations.

  • Added routing capabilities via SessionFactory and AbstractRoutingSessionFactory.

  • Introduced Update and Query objects.

  • Renamed CRUD Repository interface: CassandraRepository using MapId is now renamed to MapIdCassandraRepository. TypedIdCassandraRepository is renamed to CassandraRepository.

  • Pagination via PagingState and CassandraPageRequest.

  • Interface and DTO projections in Repository query methods.

  • Lightweight transaction support via InsertOptions and UpdateOptions using the Template API.

  • Query options for Repository query methods.

  • Introduced new annotation for @AllowFiltering.

  • Index creation on application startup via @Indexed and @SASI.

  • Tooling support for null-safety via Spring’s @NonNullApi and @Nullable annotations.

  • java.util.UUID properties default now to uuid column type, was previously timeuuid.

5.7. What’s new in Spring Data for Apache Cassandra 1.5

  • Assert compatibility with Cassandra 3.0 and Cassandra Java Driver 3.0.

  • Added configurable ProtocolVersion and QueryOptions on Cluster level.

  • Support for Optional as query method result and argument.

  • Declarative query methods using query derivation

  • Support for User-Defined types and mapped User-Defined types using @UserDefinedType.

  • The following annotations enable building custom, composed annotations: @Table, @UserDefinedType, @PrimaryKey, @PrimaryKeyClass, @PrimaryKeyColumn, @Column, @Query, @CassandraType.

Unresolved directive in index.adoc - include::../../../../../spring-data-commons/src/main/asciidoc/dependencies.adoc[leveloffset=+1] Unresolved directive in index.adoc - include::../../../../../spring-data-commons/src/main/asciidoc/repositories.adoc[leveloffset=+1]

Reference Documentation

6. Introduction

This part of the reference documentation explains the core functionality offered by Spring Data for Apache Cassandra.

Cassandra Support introduces the Cassandra module feature set.

Reactive Cassandra Support explains reactive Cassandra specifics.

Cassandra Repositories introduces repository support for Cassandra.

6.1. Spring CQL and Spring Data for Apache Cassandra Modules

Spring Data for Apache Cassandra allows interaction on both the CQL and the entity level.

The value provided by the Spring Data for Apache Cassandra abstraction is perhaps best shown by the sequence of actions outlined in the table below. The table shows which actions Spring take care of and which actions are the responsibility of you, the application developer.

Table 1. Spring Data for Apache Cassandra (CQL Core)- who does what?
Action Spring You

Define connection parameters.

X

Open the connection.

X

Specify the CQL statement.

X

Declare parameters and provide parameter values

X

Prepare and run the statement.

X

Set up the loop to iterate through the results (if any).

X

Do the work for each iteration.

X

Process any exception.

X

Close the Session.

X

The core CQL support takes care of all the low-level details that can make Cassandra and CQL such a tedious API with which to develop. Using mapped entity objects allows schema generation, object mapping, and repository support.

6.1.1. Choosing an Approach for Cassandra Database Access

You can choose among several approaches to use as a basis for your Cassandra database access. Spring’s support for Apache Cassandra comes in different flavors. Once you start using one of these approaches, you can still mix and match to include a feature from a different approach. The following approaches work well:

  • CqlTemplate and ReactiveCqlTemplate are the classic Spring CQL approach and the most popular. This is the “lowest-level” approach. Note that components like CassandraTemplate use CqlTemplate under-the-hood.

  • CassandraTemplate wraps a CqlTemplate to provide query result-to-object mapping and the use of SELECT, INSERT, UPDATE, and DELETE methods instead of writing CQL statements. This approach provides better documentation and ease of use.

  • ReactiveCassandraTemplate wraps a ReactiveCqlTemplate to provide query result-to-object mapping and the use of SELECT, INSERT, UPDATE, and DELETE methods instead of writing CQL statements. This approach provides better documentation and ease of use.

  • Repository Abstraction lets you create repository declarations in your data access layer. The goal of Spring Data’s repository abstraction is to significantly reduce the amount of boilerplate code required to implement data access layers for various persistence stores.

7. Cassandra Support

Spring Data support for Apache Cassandra contains a wide range of features:

  • Spring configuration support with Java-based @Configuration classes or the XML namespace.

  • The CqlTemplate helper class that increases productivity by properly handling common Cassandra data access operations.

  • The CassandraTemplate helper class that provides object mapping between CQL Tables and POJOs.

  • Exception translation into Spring’s portable Data Access Exception Hierarchy.

  • Feature rich object mapping integrated with Spring’s Conversion Service.

  • Annotation-based mapping metadata that is extensible to support other metadata formats.

  • Java-based query, criteria, and update DSLs.

  • Automatic implementation of Repository interfaces including support for custom finder methods.

For most data-oriented tasks, you can use the CassandraTemplate or the Repository support, both of which use the rich object-mapping functionality. CqlTemplate is commonly used to increment counters or perform ad-hoc CRUD operations. CqlTemplate also provides callback methods that make it easy to get low-level API objects, such as com.datastax.oss.driver.api.core.CqlSession, which lets you communicate directly with Cassandra. Spring Data for Apache Cassandra uses consistent naming conventions on objects in various APIs to those found in the DataStax Java Driver so that they are familiar and so that you can map your existing knowledge onto the Spring APIs.

7.1. Getting Started

Spring Data for Apache Cassandra requires Apache Cassandra 2.1 or later and Datastax Java Driver 4.0 or later. An easy way to quickly set up and bootstrap a working environment is to create a Spring-based project in STS or use Spring Initializer.

First, you need to set up a running Apache Cassandra server. See the Apache Cassandra Quick Start Guide for an explanation on how to start Apache Cassandra. Once installed, starting Cassandra is typically a matter of executing the following command: CASSANDRA_HOME/bin/cassandra -f.

To create a Spring project in STS, go to File → New → Spring Template Project → Simple Spring Utility Project and press Yes when prompted. Then enter a project and a package name, such as org.spring.data.cassandra.example.

Then you can add the following dependency declaration to your pom.xml file’s dependencies section.

<dependencies>

  <dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-cassandra</artifactId>
    <version>3.3.0-SNAPSHOT</version>
  </dependency>

</dependencies>

Also, you should change the version of Spring in the pom.xml file to be as follows:

<spring.framework.version>5.3.10</spring.framework.version>

If using a milestone release instead of a GA release, you also need to add the location of the Spring Milestone repository for Maven to your pom.xml file so that it is at the same level of your <dependencies/> element, as follows:

<repositories>
  <repository>
    <id>spring-milestone</id>
    <name>Spring Maven MILESTONE Repository</name>
    <url>https://repo.spring.io/libs-milestone</url>
  </repository>
</repositories>

The repository is also browseable here.

You can also browse all Spring repositories here.

Now you can create a simple Java application that stores and reads a domain object to and from Cassandra.

To do so, first create a simple domain object class to persist, as the following example shows:

package org.springframework.data.cassandra.example;

import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.Table;

@Table
public class Person {

  @PrimaryKey private final String id;

  private final String name;
  private final int age;

  public Person(String id, String name, int age) {
    this.id = id;
    this.name = name;
    this.age = age;
  }

  public String getId() {
    return id;
  }

  private String getName() {
    return name;
  }

  private int getAge() {
    return age;
  }

  @Override
  public String toString() {
    return String.format("{ @type = %1$s, id = %2$s, name = %3$s, age = %4$d }", getClass().getName(), getId(),
        getName(), getAge());
  }
}

Next, create the main application to run, as the following example shows:

package org.springframework.data.cassandra.example;

import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.data.cassandra.core.CassandraOperations;
import org.springframework.data.cassandra.core.CassandraTemplate;
import org.springframework.data.cassandra.core.query.Criteria;
import org.springframework.data.cassandra.core.query.Query;

import com.datastax.oss.driver.api.core.CqlSession;

public class CassandraApplication {

  private static final Logger LOGGER = LoggerFactory.getLogger(CassandraApplication.class);

  private static Person newPerson(String name, int age) {
    return new Person(UUID.randomUUID().toString(), name, age);
  }

  public static void main(String[] args) {

    CqlSession cqlSession = CqlSession.builder().withKeyspace("mykeyspace").build();

    CassandraOperations template = new CassandraTemplate(cqlSession);

    Person jonDoe = template.insert(newPerson("Jon Doe", 40));

    LOGGER.info(template.selectOne(Query.query(Criteria.where("id").is(jonDoe.getId())), Person.class).getId());

    template.truncate(Person.class);
    cqlSession.close();
  }

}

Even in this simple example, there are a few notable things to point out:

  • You can create an instance of CassandraTemplate with a Cassandra CqlSession.

  • You must annotate your POJO as a Cassandra @Table entity and also annotate the @PrimaryKey. Optionally, you can override these mapping names to match your Cassandra database table and column names.

  • You can either use raw CQL or the DataStax QueryBuilder API to construct your queries.

7.2. Examples Repository

To get a feel for how the library works, you can download and play around with several examples. .

7.3. Connecting to Cassandra with Spring

One of the first tasks when using Apache Cassandra with Spring is to create a com.datastax.oss.driver.api.core.CqlSession object by using the Spring IoC container. You can do so either by using Java-based bean metadata or by using XML-based bean metadata. These are discussed in the following sections.

For those not familiar with how to configure the Spring container using Java-based bean metadata instead of XML-based metadata, see the high-level introduction in the reference docs here as well as the detailed documentation here.

7.3.1. Registering a Session Instance by Using Java-based Metadata

The following example shows how to use Java-based bean metadata to register an instance of a com.datastax.oss.driver.api.core.CqlSession:

Example 1. Registering a com.datastax.oss.driver.api.core.CqlSession object by using Java-based bean metadata
@Configuration
public class AppConfig {

  /*
   * Use the standard Cassandra driver API to create a com.datastax.oss.driver.api.core.CqlSession instance.
   */
  public @Bean CqlSession session() {
    return CqlSession.builder().withKeyspace("mykeyspace").build();
  }
}

This approach lets you use the standard com.datastax.oss.driver.api.core.CqlSession API that you may already know.

An alternative is to register an instance of com.datastax.oss.driver.api.core.CqlSession with the container by using Spring’s CqlSessionFactoryBean. As compared to instantiating a com.datastax.oss.driver.api.core.CqlSession instance directly, the FactoryBean approach has the added advantage of also providing the container with an ExceptionTranslator implementation that translates Cassandra exceptions to exceptions in Spring’s portable DataAccessException hierarchy. This hierarchy and the use of @Repository is described in Spring’s DAO support features.

The following example shows Java-based factory class usage:

Example 2. Registering a com.datastax.oss.driver.api.core.CqlSession object by using Spring’s CqlSessionFactoryBean:
@Configuration
public class FactoryBeanAppConfig {

  /*
   * Factory bean that creates the com.datastax.oss.driver.api.core.CqlSession instance
   */
  @Bean
  public CqlSessionFactoryBean session() {

    CqlSessionFactoryBean session = new CqlSessionFactoryBean();
    session.setContactPoints("localhost");
    session.setKeyspaceName("mykeyspace");

    return session;
  }
}

Using CassandraTemplate with object mapping and repository support requires a CassandraTemplate, CassandraMappingContext, CassandraConverter, and enabling repository support.

The following example shows how to register components to configure object mapping and repository support:

Example 3. Registering components to configure object mapping and repository support
@Configuration
@EnableCassandraRepositories(basePackages = { "org.springframework.data.cassandra.example" })
public class CassandraConfig {

  @Bean
  public CqlSessionFactoryBean session() {

    CqlSessionFactoryBean session = new CqlSessionFactoryBean();
    session.setContactPoints("localhost");
    session.setKeyspaceName("mykeyspace");

    return session;
  }

  @Bean
  public SessionFactoryFactoryBean sessionFactory(CqlSession session, CassandraConverter converter) {

    SessionFactoryFactoryBean sessionFactory = new SessionFactoryFactoryBean();
    sessionFactory.setSession(session);
    sessionFactory.setConverter(converter);
    sessionFactory.setSchemaAction(SchemaAction.NONE);

    return sessionFactory;
  }

  @Bean
  public CassandraMappingContext mappingContext(CqlSession cqlSession) {

    CassandraMappingContext mappingContext = new CassandraMappingContext();
    mappingContext.setUserTypeResolver(new SimpleUserTypeResolver(cqlSession));

    return mappingContext;
  }

  @Bean
  public CassandraConverter converter(CassandraMappingContext mappingContext) {
    return new MappingCassandraConverter(mappingContext);
  }

  @Bean
  public CassandraOperations cassandraTemplate(SessionFactory sessionFactory, CassandraConverter converter) {
    return new CassandraTemplate(sessionFactory, converter);
  }
}

Creating configuration classes that register Spring Data for Apache Cassandra components can be an exhausting challenge, so Spring Data for Apache Cassandra comes with a pre-built configuration support class. Classes that extend from AbstractCassandraConfiguration register beans for Spring Data for Apache Cassandra use. AbstractCassandraConfiguration lets you provide various configuration options, such as initial entities, default query options, pooling options, socket options, and many more. AbstractCassandraConfiguration also supports you with schema generation based on initial entities, if any are provided. Extending from AbstractCassandraConfiguration requires you to at least provide the keyspace name by implementing the getKeyspaceName method. The following example shows how to register beans by using AbstractCassandraConfiguration:

Example 4. Registering Spring Data for Apache Cassandra beans by using AbstractCassandraConfiguration
@Configuration
public class CassandraConfiguration extends AbstractCassandraConfiguration {

  /*
   * Provide a contact point to the configuration.
   */
  public String getContactPoints() {
    return "localhost";
  }

  /*
   * Provide a keyspace name to the configuration.
   */
  public String getKeyspaceName() {
    return "mykeyspace";
  }
}

7.3.2. XML Configuration

This section describes how to configure Spring Data Cassandra with XML.

Externalizing Connection Properties

To externalize connection properties, you should first create a properties file that contains the information needed to connect to Cassandra. contactpoints and keyspace are the required fields.

The following example shows our properties file, called cassandra.properties:

cassandra.contactpoints=10.1.55.80:9042,10.1.55.81:9042
cassandra.keyspace=showcase

In the next two examples, we use Spring to load these properties into the Spring context.

Registering a Session Instance by using XML-based Metadata

While you can use Spring’s traditional <beans/> XML namespace to register an instance of com.datastax.oss.driver.api.core.CqlSession with the container, the XML can be quite verbose, because it is general purpose. XML namespaces are a better alternative to configuring commonly used objects, such as the CqlSession instance. The cassandra namespace let you create a CqlSession instance.

The following example shows how to configure the cassandra namespace:

Example 5. XML schema to configure Cassandra by using the cassandra namespace
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:cassandra="http://www.springframework.org/schema/data/cassandra"
  xsi:schemaLocation="
    http://www.springframework.org/schema/data/cassandra
    https://www.springframework.org/schema/data/cassandra/spring-cassandra.xsd
    http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd">

  <!-- Default bean name is 'cassandraSession' -->
  <cassandra:session contact-points="localhost" port="9042">
    <cassandra:keyspace action="CREATE_DROP" name="mykeyspace" />
  </cassandra:session>

  <cassandra:session-factory>
    <cassandra:script
            location="classpath:/org/springframework/data/cassandra/config/schema.cql"/>
  </cassandra:session-factory>
</beans>

The XML configuration elements for more advanced Cassandra configuration are shown below. These elements all use default bean names to keep the configuration code clean and readable.

While the preceding example shows how easy it is to configure Spring to connect to Cassandra, there are many other options. Basically, any option available with the DataStax Java Driver is also available in the Spring Data for Apache Cassandra configuration. This includes but is not limited to authentication, load-balancing policies, retry policies, and pooling options. All of the Spring Data for Apache Cassandra method names and XML elements are named exactly (or as close as possible) like the configuration options on the driver so that mapping any existing driver configuration should be straight forward. The following example shows how to configure Spring Data components by using XML

Example 6. Configuring Spring Data components by using XML
<!-- Loads the properties into the Spring Context and uses them to fill
in placeholders in the bean definitions -->
<context:property-placeholder location="classpath:cassandra.properties" />

<!-- REQUIRED: The Cassandra Session -->
<cassandra:session contact-points="${cassandra.contactpoints}" keyspace-name="${cassandra.keyspace}" />

<!-- REQUIRED: The default Cassandra mapping context used by `CassandraConverter` -->
<cassandra:mapping>
  <cassandra:user-type-resolver keyspace-name="${cassandra.keyspace}" />
</cassandra:mapping>

<!-- REQUIRED: The default Cassandra converter used by `CassandraTemplate` -->
<cassandra:converter />

<!-- REQUIRED: The Cassandra template is the foundation of all Spring
Data Cassandra -->
<cassandra:template id="cassandraTemplate" />

<!-- OPTIONAL: If you use Spring Data for Apache Cassandra repositories, add
your base packages to scan here -->
<cassandra:repositories base-package="org.spring.cassandra.example.repo" />

7.4. Schema Management

Apache Cassandra is a data store that requires a schema definition prior to any data interaction. Spring Data for Apache Cassandra can support you with schema creation.

7.4.1. Keyspaces and Lifecycle Scripts

The first thing to start with is a Cassandra keyspace. A keyspace is a logical grouping of tables that share the same replication factor and replication strategy. Keyspace management is located in the CqlSession configuration, which has the KeyspaceSpecification and startup and shutdown CQL script execution.

Declaring a keyspace with a specification allows creating and dropping of the Keyspace. It derives CQL from the specification so that you need not write CQL yourself. The following example specifies a Cassadra keyspace by using XML:

Example 7. Specifying a Cassandra keyspace by using XML
<cassandra:session>

    <cassandra:keyspace action="CREATE_DROP" durable-writes="true" name="my_keyspace">
        <cassandra:replication class="NETWORK_TOPOLOGY_STRATEGY">
          <cassandra:data-center name="foo" replication-factor="1" />
          <cassandra:data-center name="bar" replication-factor="2" />
        </cassandra:replication>
  </cassandra:keyspace>

</cassandra:session>

You can also specify a Cassandra keyspace by using Java configuration, as the following example shows:

Example 8. Specifying a Cassandra keyspace by using Java configuration
@Configuration
public class CreateKeyspaceConfiguration extends AbstractCassandraConfiguration implements BeanClassLoaderAware {

  @Override
  protected List<CreateKeyspaceSpecification> getKeyspaceCreations() {

    CreateKeyspaceSpecification specification = CreateKeyspaceSpecification.createKeyspace("my_keyspace")
        .with(KeyspaceOption.DURABLE_WRITES, true)
        .withNetworkReplication(DataCenterReplication.of("foo", 1), DataCenterReplication.of("bar", 2));

    return Arrays.asList(specification);
  }

  @Override
  protected List<DropKeyspaceSpecification> getKeyspaceDrops() {
    return Arrays.asList(DropKeyspaceSpecification.dropKeyspace("my_keyspace"));
  }

  // ...
}
Keyspace creation allows rapid bootstrapping without the need of external keyspace management. This can be useful for certain scenarios but should be used with care. Dropping a keyspace on application shutdown removes the keyspace and all data from the tables in the keyspace.

7.4.2. Initializing a SessionFactory

The org.springframework.data.cassandra.core.cql.session.init package provides support for initializing an existing SessionFactory. You may sometimes need to initialize a keyspace that runs on a server somewhere.

Initializing a Keyspace

You can provide arbitrary CQL that is executed on CqlSession initialization and shutdown in the configured keyspace, as the following Java configuration example shows:

@Configuration
public class KeyspacePopulatorConfiguration extends AbstractCassandraConfiguration {

  @Nullable
  @Override
  protected KeyspacePopulator keyspacePopulator() {
    return new ResourceKeyspacePopulator(scriptOf("CREATE TABLE my_table …"));
  }

  @Nullable
  @Override
  protected KeyspacePopulator keyspaceCleaner() {
    return new ResourceKeyspacePopulator(scriptOf("DROP TABLE my_table;"));
  }

  // ...
}

If you want to initialize a database using XML configuration and you can provide a reference to a SessionFactory bean, you can use the initialize-keyspace tag in the cassandra namespace:

<cassandra:initialize-keyspace session-factory-ref="cassandraSessionFactory">
    <cassandra:script location="classpath:com/foo/cql/db-schema.cql"/>
    <cassandra:script location="classpath:com/foo/cql/db-test-data.cql"/>
</cassandra:initialize-keyspace>

The preceding example runs the two specified scripts against the keyspace. The first script creates a schema, and the second populates tables with a test data set. The script locations can also be patterns with wildcards in the usual Ant style used for resources in Spring (for example, classpath*:/com/foo/**/cql/*-data.cql). If you use a pattern, the scripts are run in the lexical order of their URL or filename.

The default behavior of the keyspace initializer is to unconditionally run the provided scripts. This may not always be what you want — for instance, if you run the scripts against a keyspace that already has test data in it. The likelihood of accidentally deleting data is reduced by following the common pattern (shown earlier) of creating the tables first and then inserting the data. The first step fails if the tables already exist.

However, to gain more control over the creation and deletion of existing data, the XML namespace provides a few additional options. The first is a flag to switch the initialization on and off. You can set this according to the environment (such as pulling a boolean value from system properties or from an environment bean). The following example gets a value from a system property:

<cassandra:initialize-keyspace session-factory-ref="cassandraSessionFactory"
    enabled="#{systemProperties.INITIALIZE_KEYSPACE}">    (1)
    <cassandra:script location="..."/>
</cassandra:initialize-database>
1 Get the value for enabled from a system property called INITIALIZE_KEYSPACE.

The second option to control what happens with existing data is to be more tolerant of failures. To this end, you can control the ability of the initializer to ignore certain errors in the CQL it executes from the scripts, as the following example shows:

<cassandra:initialize-keyspace session-factory-ref="cassandraSessionFactory" ignore-failures="DROPS">
    <cassandra:script location="..."/>
</cassandra:initialize-database>

In the preceding example, we are saying that we expect that, sometimes, the scripts are run against an empty keyspace, and there are some DROP statements in the scripts that would, therefore, fail. So failed CQL DROP statements will be ignored, but other failures will cause an exception. This is useful if you don’t want tu use support DROP …​ IF EXISTS (or similar) but you want to unconditionally remove all test data before re-creating it. In that case the first script is usually a set of DROP statements, followed by a set of CREATE statements.

The ignore-failures option can be set to NONE (the default), DROPS (ignore failed drops), or ALL (ignore all failures).

Each statement should be separated by ; or a new line if the ; character is not present at all in the script. You can control that globally or script by script, as the following example shows:

@Configuration
public class SessionFactoryInitializerConfiguration extends AbstractCassandraConfiguration {

  @Bean
  SessionFactoryInitializer sessionFactoryInitializer(SessionFactory sessionFactory) {

    SessionFactoryInitializer initializer = new SessionFactoryInitializer();
    initializer.setSessionFactory(sessionFactory);

    ResourceKeyspacePopulator populator1 = new ResourceKeyspacePopulator();
    populator1.setSeparator(";");
    populator1.setScripts(new ClassPathResource("com/myapp/cql/db-schema.cql"));

    ResourceKeyspacePopulator populator2 = new ResourceKeyspacePopulator();
    populator2.setSeparator("@@");
    populator2.setScripts(new ClassPathResource("classpath:com/myapp/cql/db-test-data-1.cql"), //
        new ClassPathResource("classpath:com/myapp/cql/db-test-data-2.cql"));

    initializer.setKeyspacePopulator(new CompositeKeyspacePopulator(populator1, populator2));

    return initializer;
  }

  // ...
}

Alternatively, you can use XML to configure the SessionFactoryInitializer:

<cassandra:initialize-keyspace session-factory-ref="cassandraSessionFactory" separator="@@">        (1)
    <cassandra:script location="classpath:com/myapp/cql/db-schema.cql" separator=";"/>     (2)
    <cassandra:script location="classpath:com/myapp/cql/db-test-data-1.cql"/>
    <cassandra:script location="classpath:com/myapp/cql/db-test-data-2.cql"/>
</cassandra:initialize-keyspace>
1 Set the separator scripts to @@.
2 Set the separator for db-schema.cql to ;.

In this example, the two test-data scripts use @@ as statement separator and only the db-schema.cql uses ;. This configuration specifies that the default separator is @@ and overrides that default for the db-schema script.

If you need more control than you get from the XML namespace, you can use the SessionFactoryInitializer directly and define it as a component in your application.

Initialization of Other Components that Depend on the Keyspace

A large class of applications (those that do not use the database until after the Spring context has started) can use the database initializer with no further complications. If your application is not one of those, you might need to read the rest of this section.

The database initializer depends on a SessionFactory instance and runs the scripts provided in its initialization callback (analogous to an init-method in an XML bean definition, a @PostConstruct method in a component, or the afterPropertiesSet() method in a component that implements InitializingBean). If other beans depend on the same data source and use the session factory in an initialization callback, there might be a problem because the data has not yet been initialized. A common example of this is a cache that initializes eagerly and loads data from the database on application startup.

To get around this issue, you have two options: change your cache initialization strategy to a later phase or ensure that the keyspace initializer is initialized first.

Changing your cache initialization strategy might be easy if the application is in your control and not otherwise. Some suggestions for how to implement this include:

  • Make the cache initialize lazily on first usage, which improves application startup time.

  • Have your cache or a separate component that initializes the cache implement Lifecycle or SmartLifecycle. When the application context starts, you can automatically start a SmartLifecycle by setting its autoStartup flag, and you can manually start a Lifecycle by calling ConfigurableApplicationContext.start() on the enclosing context.

  • Use a Spring ApplicationEvent or similar custom observer mechanism to trigger the cache initialization. ContextRefreshedEvent is always published by the context when it is ready for use (after all beans have been initialized), so that is often a useful hook (this is how the SmartLifecycle works by default).

Ensuring that the keyspace initializer is initialized first can also be easy. Some suggestions on how to implement this include:

  • Rely on the default behavior of the Spring BeanFactory, which is that beans are initialized in registration order. You can easily arrange that by adopting the common practice of a set of <import/> elements in XML configuration that order your application modules and ensuring that the database and database initialization are listed first.

  • Separate the SessionFactory and the business components that use it and control their startup order by putting them in separate ApplicationContext instances (for example, the parent context contains the SessionFactory, and the child context contains the business components). This structure is common in Spring web applications but can be more generally applied.

  • Use the Schema management for Tables and User-defined Types to initialize the keyspace using Spring Data Cassandra’s built-in schema generator.

7.4.3. Tables and User-defined Types

Spring Data for Apache Cassandra approaches data access with mapped entity classes that fit your data model. You can use these entity classes to create Cassandra table specifications and user type definitions.

Schema creation is tied to CqlSession initialization by SchemaAction. The following actions are supported:

  • SchemaAction.NONE: No tables or types are created or dropped. This is the default setting.

  • SchemaAction.CREATE: Create tables, indexes, and user-defined types from entities annotated with @Table and types annotated with @UserDefinedType. Existing tables or types cause an error if you tried to create the type.

  • SchemaAction.CREATE_IF_NOT_EXISTS: Like SchemaAction.CREATE but with IF NOT EXISTS applied. Existing tables or types do not cause any errors but may remain stale.

  • SchemaAction.RECREATE: Drops and recreates existing tables and types that are known to be used. Tables and types that are not configured in the application are not dropped.

  • SchemaAction.RECREATE_DROP_UNUSED: Drops all tables and types and recreates only known tables and types.

SchemaAction.RECREATE and SchemaAction.RECREATE_DROP_UNUSED drop your tables and lose all data. RECREATE_DROP_UNUSED also drops tables and types that are not known to the application.
Enabling Tables and User-Defined Types for Schema Management

Metadata-based Mapping explains object mapping with conventions and annotations. To prevent unwanted classes from being created as a table or a type, schema management is only active for entities annotated with @Table and user-defined types annotated with @UserDefinedType. Entities are discovered by scanning the classpath. Entity scanning requires one or more base packages. Tuple-typed columns that use TupleValue do not provide any typing details. Consequently, you must annotate such column properties with @CassandraType(type = TUPLE, typeArguments = …) to specify the desired column type.

The following example shows how to specify entity base packages in XML configuration:

Example 9. Specifying entity base packages with XML configuration
<cassandra:mapping entity-base-packages="com.foo,com.bar"/>

The following example shows how to specify entity base packages in Java configuration:

Example 10. Specifying entity base packages with Java configuration
@Configuration
public class EntityBasePackagesConfiguration extends AbstractCassandraConfiguration {

  @Override
  public String[] getEntityBasePackages() {
    return new String[] { "com.foo", "com.bar" };
  }

  // ...
}

7.5. CqlTemplate

The CqlTemplate class is the central class in the core CQL package. It handles the creation and release of resources. It performs the basic tasks of the core CQL workflow, such as statement creation and execution, and leaves application code to provide CQL and extract results. The CqlTemplate class executes CQL queries and update statements, performs iteration over ResultSet instances and extraction of returned parameter values. It also catches CQL exceptions and translates them to the generic, more informative, exception hierarchy defined in the org.springframework.dao package.

When you use the CqlTemplate for your code, you need only implement callback interfaces, which have a clearly defined contract. Given a Connection, the PreparedStatementCreator callback interface creates a prepared statement with the provided CQL and any necessary parameter arguments. The RowCallbackHandler interface extracts values from each row of a ResultSet.

The CqlTemplate can be used within a DAO implementation through direct instantiation with a SessionFactory reference or be configured in the Spring container and given to DAOs as a bean reference. CqlTemplate is a foundational building block for CassandraTemplate.

All CQL issued by this class is logged at the DEBUG level under the category corresponding to the fully-qualified class name of the template instance (typically CqlTemplate, but it may be different if you use a custom subclass of the CqlTemplate class).

You can control fetch size, consistency level, and retry policy defaults by configuring these parameters on the CQL API instances: CqlTemplate, AsyncCqlTemplate, and ReactiveCqlTemplate. Defaults apply if the particular query option is not set.

CqlTemplate comes in different execution model flavors. The basic CqlTemplate uses a blocking execution model. You can use AsyncCqlTemplate for asynchronous execution and synchronization with ListenableFuture instances or ReactiveCqlTemplate for reactive execution.

7.5.1. Examples of CqlTemplate Class Usage

This section provides some examples of the CqlTemplate class in action. These examples are not an exhaustive list of all of the functionality exposed by the CqlTemplate. See the Javadoc for that.

Querying (SELECT) with CqlTemplate

The following query gets the number of rows in a table:

int rowCount = cqlTemplate.queryForObject("SELECT COUNT(*) FROM t_actor", Integer.class);

The following query uses a bind variable:

int countOfActorsNamedJoe = cqlTemplate.queryForObject(
    "SELECT COUNT(*) FROM t_actor WHERE first_name = ?", Integer.class, "Joe");

The following example queries for a String:

String lastName = cqlTemplate.queryForObject(
    "SELECT last_name FROM t_actor WHERE id = ?",
    String.class, 1212L);

The following example queries and populates a single domain object:

Actor actor = cqlTemplate.queryForObject("SELECT first_name, last_name FROM t_actor WHERE id = ?",
    new RowMapper<Actor>() {
      public Actor mapRow(Row row, int rowNum) {
        Actor actor = new Actor();
        actor.setFirstName(row.getString("first_name"));
        actor.setLastName(row.getString("last_name"));
        return actor;
      }
    }, 1212L);

The following example queries and populates multiple domain objects:

List<Actor> actors = cqlTemplate.query(
    "SELECT first_name, last_name FROM t_actor",
    new RowMapper<Actor>() {
      public Actor mapRow(Row row, int rowNum) {
        Actor actor = new Actor();
        actor.setFirstName(row.getString("first_name"));
        actor.setLastName(row.getString("last_name"));
        return actor;
      }
    });

If the last two snippets of code actually existed in the same application, it would make sense to remove the duplication present in the two RowMapper anonymous inner classes and extract them out into a single class (typically a static nested class) that can then be referenced by DAO methods.

For example, it might be better to write the last code snippet as follows:

List<Actor> findAllActors() {
  return cqlTemplate.query("SELECT first_name, last_name FROM t_actor", ActorMapper.INSTANCE);
}

enum ActorMapper implements RowMapper<Actor> {

  INSTANCE;

  public Actor mapRow(Row row, int rowNum) {
    Actor actor = new Actor();
    actor.setFirstName(row.getString("first_name"));
    actor.setLastName(row.getString("last_name"));
    return actor;
  }
}
INSERT, UPDATE, and DELETE with CqlTemplate

You can use the execute(…) method to perform INSERT, UPDATE, and DELETE operations. Parameter values are usually provided as variable arguments or, alternatively, as an object array.

The following example shows how to perform an INSERT operation with CqlTemplate:

cqlTemplate.execute(
    "INSERT INTO t_actor (first_name, last_name) VALUES (?, ?)",
    "Leonor", "Watling");

The following example shows how to perform an UPDATE operation with CqlTemplate:

cqlTemplate.execute(
    "UPDATE t_actor SET last_name = ? WHERE id = ?",
    "Banjo", 5276L);

The following example shows how to perform an DELETE operation with CqlTemplate:

cqlTemplate.execute(
    "DELETE FROM t_actor WHERE id = ?",
    5276L);
Other CqlTemplate operations

You can use the execute(..) method to execute any arbitrary CQL. As a result, the method is often used for DDL statements. It is heavily overloaded with variants that take callback interfaces, bind variable arrays, and so on.

The following example shows how to create and drop a table by using different API objects that are all passed to the execute() methods:

    cqlTemplate.execute("CREATE TABLE test_table (id uuid primary key, event text)");

    DropTableSpecification dropper = DropTableSpecification.dropTable("test_table");
    String cql = DropTableCqlGenerator.toCql(dropper);

    cqlTemplate.execute(cql);

7.6. Exception Translation

The Spring Framework provides exception translation for a wide variety of database and mapping technologies. This has traditionally been for JDBC and JPA. Spring Data for Apache Cassandra extends this feature to Apache Cassandra by providing an implementation of the org.springframework.dao.support.PersistenceExceptionTranslator interface.

The motivation behind mapping to Spring’s consistent data access exception hierarchy is to let you write portable and descriptive exception handling code without resorting to coding against and handling specific Cassandra exceptions. All of Spring’s data access exceptions are inherited from the DataAccessException class, so you can be sure that you can catch all database-related exceptions within a single try-catch block.

7.7. Controlling Cassandra Connections

Applications connect to Apache Cassandra by using CqlSession objects. A Cassandra CqlSession keeps track of multiple connections to the individual nodes and is designed to be a thread-safe, long-lived object. Usually, you can use a single CqlSession for the whole application.

Spring acquires a Cassandra CqlSession through a SessionFactory. SessionFactory is part of Spring Data for Apache Cassandra and is a generalized connection factory. It lets the container or framework hide connection handling and routing issues from the application code.

The following example shows how to configure a default SessionFactory:

Session session = … // get a Cassandra Session

CqlTemplate template = new CqlTemplate();

template.setSessionFactory(new DefaultSessionFactory(session));

CqlTemplate and other Template API implementations obtain a CqlSession for each operation. Due to their long-lived nature, sessions are not closed after invoking the desired operation. Responsibility for proper resource disposal lies with the container or framework that uses the session.

You can find various SessionFactory implementations within the org.springframework.data.cassandra.core.cql.session package.

7.8. Introduction to CassandraTemplate

The CassandraTemplate class, located in the org.springframework.data.cassandra package, is the central class in Spring’s Cassandra support and provides a rich feature set to interact with the database. The template offers convenience operations to create, update, delete, and query Cassandra, and provides a mapping between your domain objects and rows in Cassandra tables.

Once configured, CassandraTemplate is thread-safe and can be reused across multiple instances.

The mapping between rows in Cassandra and application domain classes is done by delegating to an implementation of the CassandraConverter interface. Spring provides a default implementation, MappingCassandraConverter, but you can also write your own custom converter. See the section on Cassandra conversion for more detailed information.

The CassandraTemplate class implements the CassandraOperations interface. In as much as possible, the methods on CassandraOperations are named after methods available in Cassandra to make the API familiar to developers who are already familiar with Cassandra.

For example, you can find methods such as select, insert, delete, and update. The design goal was to make it as easy as possible to transition between the use of the base Cassandra driver and CassandraOperations. A major difference between the two APIs is that CassandraOperations can be passed domain objects instead of CQL and query objects.

The preferred way to reference operations on a CassandraTemplate instance is through the CassandraOperations interface.

The default converter implementation used by CassandraTemplate is MappingCassandraConverter. While MappingCassandraConverter can use additional metadata to specify the mapping of objects to rows, it can also convert objects that contain no additional metadata by using some conventions for the mapping of fields and table names. These conventions, as well as the use of mapping annotations, are explained in the “Mapping” chapter.

Another central feature of CassandraTemplate is exception translation of exceptions thrown in the Cassandra Java driver into Spring’s portable Data Access Exception hierarchy. See the section on exception translation for more information.

The Template API has different execution model flavors. The basic CassandraTemplate uses a blocking (imperative-synchronous) execution model. You can use AsyncCassandraTemplate for asynchronous execution and synchronization with ListenableFuture instances or ReactiveCassandraTemplate for reactive execution.

7.8.1. Instantiating CassandraTemplate

CassandraTemplate should always be configured as a Spring bean, although we show an example earlier where you can instantiate it directly. However, because we are assuming the context of making a Spring module, we assume the presence of the Spring container.

There are two ways to get a CassandraTemplate, depending on how you load you Spring ApplicationContext:

Autowiring

You can autowire a CassandraOperations into your project, as the following example shows:

@Autowired
private CassandraOperations cassandraOperations;

As with all Spring autowiring, this assumes there is only one bean of type CassandraOperations in the ApplicationContext. If you have multiple CassandraTemplate beans (which is the case if you work with multiple keyspaces in the same project), then you can use the @Qualifier annotation to designate the bean you want to autowire.

@Autowired
@Qualifier("keyspaceOneTemplateBeanId")
private CassandraOperations cassandraOperations;
Bean Lookup with ApplicationContext

You can also look up the CassandraTemplate bean from the ApplicationContext, as shown in the following example:

CassandraOperations cassandraOperations = applicationContext.getBean("cassandraTemplate", CassandraOperations.class);

7.9. Saving, Updating, and Removing Rows

CassandraTemplate provides a simple way for you to save, update, and delete your domain objects and map those objects to tables managed in Cassandra.

7.9.1. Type Mapping

Spring Data for Apache Cassandra relies on the DataStax Java driver’s CodecRegistry to ensure type support. As types are added or changed, the Spring Data for Apache Cassandra module continues to function without requiring changes. See CQL data types and “Data Mapping and Type Conversion” for the current type mapping matrix.

7.9.2. Methods for Inserting and Updating rows

CassandraTemplate has several convenient methods for saving and inserting your objects. To have more fine-grained control over the conversion process, you can register Spring Converter instances with the MappingCassandraConverter (for example, Converter<Row, Person>).

The difference between insert and update operations is that INSERT operations do not insert null values.

The simple case of using the INSERT operation is to save a POJO. In this case, the table name is determined by the simple class name (not the fully qualified class name). The table to store the object can be overridden by using mapping metadata.

When inserting or updating, the id property must be set. Apache Cassandra has no means to generate an ID.

The following example uses the save operation and retrieves its contents:

Example 11. Inserting and retrieving objects by using the CassandraTemplate
import static org.springframework.data.cassandra.core.query.Criteria.where;
import static org.springframework.data.cassandra.core.query.Query.query;
…

Person bob = new Person("Bob", 33);
cassandraTemplate.insert(bob);

Person queriedBob = cassandraTemplate.selectOneById(query(where("age").is(33)), Person.class);

You can use the following operations to insert and save:

  • void insert (Object objectToSave): Inserts the object in an Apache Cassandra table.

  • WriteResult insert (Object objectToSave, InsertOptions options): Inserts the object in an Apache Cassandra table and applies InsertOptions.

You can use the following update operations:

  • void update (Object objectToSave): Updates the object in an Apache Cassandra table.

  • WriteResult update (Object objectToSave, UpdateOptions options): Updates the object in an Apache Cassandra table and applies UpdateOptions.

You can also use the old fashioned way and write your own CQL statements, as the following example shows:

String cql = "INSERT INTO person (age, name) VALUES (39, 'Bob')";

cassandraTemplate().getCqlOperations().execute(cql);

You can also configure additional options such as TTL, consistency level, and lightweight transactions when using InsertOptions and UpdateOptions.

Which Table Are My Rows Inserted into?

You can manage the table name that is used for operating on the tables in two ways. The default table name is the simple class name changed to start with a lower-case letter. So, an instance of the com.example.Person class would be stored in the person table. The second way is to specify a table name in the @Table annotation.

Inserting, Updating, and Deleting Individual Objects in a Batch

The Cassandra protocol supports inserting a collection of rows in one operation by using a batch.

The following methods in the CassandraTemplate interface support this functionality:

  • batchOps: Creates a new CassandraBatchOperations to populate the batch.

CassandraBatchOperations

  • insert: Takes a single object, an array (var-args), or an Iterable of objects to insert.

  • update: Takes a single object, an array (var-args), or an Iterable of objects to update.

  • delete: Takes a single object, an array (var-args), or an Iterable of objects to delete.

  • withTimestamp: Applies a TTL to the batch.

  • execute: Executes the batch.

7.9.3. Updating Rows in a Table

For updates, you can select to update a number of rows.

The following example shows updating a single account object by adding a one-time $50.00 bonus to the balance with the + assignment:

Example 12. Updating rows using CasandraTemplate
import static org.springframework.data.cassandra.core.query.Criteria.where;
import org.springframework.data.cassandra.core.query.Query;
import org.springframework.data.cassandra.core.query.Update;

…

boolean applied = cassandraTemplate.update(Query.query(where("id").is("foo")),
  Update.create().increment("balance", 50.00), Account.class);

In addition to the Query discussed earlier, we provide the update definition by using an Update object. The Update class has methods that match the update assignments available for Apache Cassandra.

Most methods return the Update object to provide a fluent API for code styling purposes.

Methods for Executing Updates for Rows

The update method can update rows, as follows:

  • boolean update (Query query, Update update, Class<?> entityClass): Updates a selection of objects in the Apache Cassandra table.

Methods for the Update class

The Update class can be used with a little 'syntax sugar', as its methods are meant to be chained together. Also, you can kick-start the creation of a new Update instance with the static method public static Update update(String key, Object value) and by using static imports.

The Update class has the following methods:

  • AddToBuilder addTo (String columnName) AddToBuilder entry-point:

    • Update prepend(Object value): Prepends a collection value to the existing collection by using the + update assignment.

    • Update prependAll(Object…​ values): Prepends all collection values to the existing collection by using the + update assignment.

    • Update append(Object value): Appends a collection value to the existing collection by using the + update assignment.

    • Update append(Object…​ values): Appends all collection values to the existing collection by using the + update assignment.

    • Update entry(Object key, Object value): Adds a map entry by using the + update assignment.

    • Update addAll(Map<? extends Object, ? extends Object> map): Adds all map entries to the map by using the + update assignment.

  • Update remove (String columnName, Object value): Removes the value from the collection by using the - update assignment.

  • Update clear (String columnName): Clears the collection.

  • Update increment (String columnName, Number delta): Updates by using the + update assignment.

  • Update decrement (String columnName, Number delta): Updates by using the - update assignment.

  • Update set (String columnName, Object value): Updates by using the = update assignment.

  • SetBuilder set (String columnName) SetBuilder entry-point:

    • Update atIndex(int index).to(Object value): Sets a collection at the given index to a value using the = update assignment.

    • Update atKey(String object).to(Object value): Sets a map entry at the given key to a value the = update assignment.

The following listing shows a few update examples:

// UPDATE … SET key = 'Spring Data';
Update.update("key", "Spring Data")

// UPDATE … SET key[5] = 'Spring Data';
Update.empty().set("key").atIndex(5).to("Spring Data");

// UPDATE … SET key = key + ['Spring', 'DATA'];
Update.empty().addTo("key").appendAll("Spring", "Data");

Note that Update is immutable once created. Invoking methods creates new immutable (intermediate) Update objects.

7.9.4. Methods for Removing Rows

You can use the following overloaded methods to remove an object from the database:

  • boolean delete (Query query, Class<?> entityClass): Deletes the objects selected by Query.

  • T delete (T entity): Deletes the given object.

  • T delete (T entity, QueryOptions queryOptions): Deletes the given object applying QueryOptions.

  • boolean deleteById (Object id, Class<?> entityClass): Deletes the object using the given Id.

7.9.5. Optimistic Locking

The @Version annotation provides syntax similar to that of JPA in the context of Cassandra and makes sure updates are only applied to rows with a matching version. Optimistic Locking leverages Cassandra’s lightweight transactions to conditionally insert, update and delete rows. Therefore, INSERT statements are executed with the IF NOT EXISTS condition. For updates and deletes, the actual value of the version property is added to the UPDATE condition in such a way that the modification does not have any effect if another operation altered the row in the meantime. In that case, an OptimisticLockingFailureException is thrown. The following example shows these features:

@Table
class Person {

  @Id String id;
  String firstname;
  String lastname;
  @Version Long version;
}

Person daenerys = template.insert(new Person("Daenerys"));                            (1)

Person tmp = template.findOne(query(where("id").is(daenerys.getId())), Person.class); (2)

daenerys.setLastname("Targaryen");
template.save(daenerys);                                                              (3)

template.save(tmp); // throws OptimisticLockingFailureException                       (4)
1 Intially insert document. version is set to 0.
2 Load the just inserted document. version is still 0.
3 Update the document with version = 0. Set the lastname and bump version to 1.
4 Try to update the previously loaded document that still has version = 0. The operation fails with an OptimisticLockingFailureException, as the current version is 1.
Optimistic Locking is only supported with single-entity operations and not for batch operations.

7.10. Querying Rows

You can express your queries by using the Query and Criteria classes, which have method names that reflect the native Cassandra predicate operator names, such as lt, lte, is, and others.

The Query and Criteria classes follow a fluent API style so that you can easily chain together multiple method criteria and queries while having easy-to-understand code. Static imports are used in Java when creating Query and Criteria instances to improve readability.

7.10.1. Querying Rows in a Table

In earlier sections, we saw how to retrieve a single object by using the selectOneById method on CassandraTemplate. Doing so returns a single domain object. We can also query for a collection of rows to be returned as a list of domain objects. Assuming we have a number of Person objects with name and age values stored as rows in a table and that each person has an account balance, we can now run a query by using the following code:

Example 13. Querying for rows using CassandraTemplate
import static org.springframework.data.cassandra.core.query.Criteria.where;
import static org.springframework.data.cassandra.core.query.Query.query;

…

List<Person> result = cassandraTemplate.select(query(where("age").is(50))
  .and(where("balance").gt(1000.00d)).withAllowFiltering(), Person.class);

The select, selectOne, and stream methods take a Query object as a parameter. This object defines the criteria and options used to perform the query. The criteria is specified by using a Criteria object that has a static factory method named where that instantiates a new Criteria object. We recommend using a static import for org.springframework.data.cassandra.core.query.Criteria.where and Query.query, to make the query more readable.

This query should return a list of Person objects that meet the specified criteria. The Criteria class has the following methods that correspond to the operators provided in Apache Cassandra:

Methods for the Criteria class
  • CriteriaDefinition gt (Object value): Creates a criterion by using the > operator.

  • CriteriaDefinition gte (Object value): Creates a criterion by using the >= operator.

  • CriteriaDefinition in (Object…​ values): Creates a criterion by using the IN operator for a varargs argument.

  • CriteriaDefinition in (Collection<?> collection): Creates a criterion by using the IN operator using a collection.

  • CriteriaDefinition is (Object value): Creates a criterion by using field matching (column = value).

  • CriteriaDefinition lt (Object value): Creates a criterion by using the < operator.

  • CriteriaDefinition lte (Object value): Creates a criterion by using the operator.

  • CriteriaDefinition like (Object value): Creates a criterion by using the LIKE operator.

  • CriteriaDefinition contains (Object value): Creates a criterion by using the CONTAINS operator.

  • CriteriaDefinition containsKey (Object key): Creates a criterion by using the CONTAINS KEY operator.

Criteria is immutable once created.

Methods for the Query class

The Query class has some additional methods that you can use to provide options for the query:

  • Query by (CriteriaDefinition…​ criteria): Used to create a Query object.

  • Query and (CriteriaDefinition criteria): Used to add additional criteria to the query.

  • Query columns (Columns columns): Used to define columns to be included in the query results.

  • Query limit (long limit): Used to limit the size of the returned results to the provided limit (used for paging).

  • Query pageRequest (Pageable pageRequest): Used to associate Sort, PagingState, and fetchSize with the query (used for paging).

  • Query pagingState (ByteBuffer pagingState): Used to associate a ByteBuffer with the query (used for paging).

  • Query queryOptions (QueryOptions queryOptions): Used to associate QueryOptions with the query.

  • Query sort (Sort sort): Used to provide a sort definition for the results.

  • Query withAllowFiltering (): Used to render ALLOW FILTERING queries.

Query is immutable once created. Invoking methods creates new immutable (intermediate) Query objects.

7.10.2. Methods for Querying for Rows

The Query class has the following methods that return rows:

  • List<T> select (Query query, Class<T> entityClass): Query for a list of objects of type T from the table.

  • T selectOne (Query query, Class<T> entityClass): Query for a single object of type T from the table.

  • Slice<T> slice (Query query, Class<T> entityClass): Starts or continues paging by querying for a Slice of objects of type T from the table.

  • Stream<T> stream (Query query, Class<T> entityClass): Query for a stream of objects of type T from the table.

  • List<T> select (String cql, Class<T> entityClass): Ad-hoc query for a list of objects of type T from the table by providing a CQL statement.

  • T selectOne (String cql, Class<T> entityClass): Ad-hoc query for a single object of type T from the table by providing a CQL statement.

  • Stream<T> stream (String cql, Class<T> entityClass): Ad-hoc query for a stream of objects of type T from the table by providing a CQL statement.

The query methods must specify the target type T that is returned.

7.10.3. Fluent Template API

The CassandraOperations interface is one of the central components when it comes to more low-level interaction with Apache Cassandra. It offers a wide range of methods. You can find multiple overloads for every method. Most of them cover optional (nullable) parts of the API.

FluentCassandraOperations provide a more narrow interface for common methods of CassandraOperations providing a more readable, fluent API. The entry points (query(…), insert(…), update(…), and delete(…)) follow a natural naming scheme based on the operation to execute. Moving on from the entry point, the API is designed to offer only context-dependent methods that guide the developer towards a terminating method that invokes the actual CassandraOperation. The following example shows the fluent API:

List<SWCharacter> all = ops.query(SWCharacter.class)
  .inTable("star_wars")                        (1)
  .all();
1 Skip this step if SWCharacter defines the table name with @Table or if using the class name as the table name is not a problem.

If a table in Cassandra holds entities of different types, such as a Jedi within a Table of SWCharacters, you can use different types to map the query result. You can use as(Class<?> targetType) to map results to a different target type, while query(Class<?> entityType) still applies to the query and table name. The following example uses the query and as methods:

List<Jedi> all = ops.query(SWCharacter.class)    (1)
  .as(Jedi.class)                                (2)
  .matching(query(where("jedi").is(true)))
  .all();
1 The query fields are mapped against the SWCharacter type.
2 Resulting rows are mapped into Jedi.
You can directly apply [projections] to resulting documents by providing only the interface type through as(Class<?>).

The terminating methods (first(), one(), all(), and stream()) handle switching between retrieving a single entity and retrieving multiple entities as List or Stream and similar operations.

The new fluent template API methods (that is, query(..), insert(..), update(..), and delete(..)) use effectively thread-safe supporting objects to compose the CQL statement. However, it comes at the added cost of additional young-gen JVM heap overhead, since the design is based on final fields for the various CQL statement components and construction on mutation. You should be careful when possibly inserting or deleting a large number of objects (such as inside of a loop, for instance).

7.11. Prepared Statements

CQL statements that are executed multiple times can be prepared and stored in a PreparedStatement object to improve query performance. Both, the driver and Cassandra maintain a mapping of PreparedStatement queries to their metadata. You can use prepared statements through the following abstractions:

  • CqlTemplate through the choice of API

  • CassandraTemplate by enabling prepared statements

  • Cassandra repositories as they are built on CassandraTemplate

7.11.1. Using CqlTemplate

The CqlTemplate class (and its asynchronous and reactive variants) offers various methods accepting static CQL, Statement objects and PreparedStatementCreator. Methods accepting static CQL without additional arguments typically run the CQL statement as-is without further processing. Methods accepting static CQL in combination with an arguments array (such as execute(String cql, Object…​ args) and queryForRows(String cql, Object…​ args)) use prepared statements. Internally, these methods create a PreparedStatementCreator and PreparedStatementBinder objects to prepare the statement and later on to bind values to the statement to run it. Spring Data Cassandra generally uses index-based parameter bindings for prepared statements.

Since Cassandra Driver version 4, prepared statements are cached on the driver level which removes the need to keep track of prepared statements in the application.

The following example shows how to issue a query with a parametrized prepared statement:

String lastName = cqlTemplate.queryForObject(
    "SELECT last_name FROM t_actor WHERE id = ?",
    String.class, 1212L);

In cases where you require more control over statement preparation and parameter binding (for example, using named binding parameters), you can fully control prepared statement creation and parameter binding by calling query methods with PreparedStatementCreator and PreparedStatementBinder arguments:

List<String> lastNames = cqlTemplate.query(
    session -> session.prepare("SELECT last_name FROM t_actor WHERE id = ?"),
    ps -> ps.bind(1212L),
    (row, rowNum) -> row.getString(0));

Spring Data Cassandra ships with classes supporting that pattern in the cql package:

  • SimplePreparedStatementCreator - utility class to create a prepared statement.

  • ArgumentPreparedStatementBinder - utility class to bind arguments to a prepared statement.

7.11.2. Using CassandraTemplate

The CassandraTemplate class is built on top of CqlTemplate to provide a higher level of abstraction. The use of prepared statements can be controlled directly on CassandraTemplate (and its asynchronous and reactive variants) by calling setUsePreparedStatements(false) respective setUsePreparedStatements(true). Note that the use of prepared statements by CassandraTemplate is enabled by default.

The following example shows the use of methods that generate and that accept CQL:

template.setUsePreparedStatements(true);

Actor actorByQuery = template.selectOne(query(where("id").is(42)), Actor.class);

Actor actorByStatement = template.selectOne(
    SimpleStatement.newInstance("SELECT id, name FROM actor WHERE id = ?", 42),
    Actor.class);

Calling entity-bound methods such as select(Query, Class<T>) or update(Query, Update, Class<T>) build CQL statements themselves to perform the intended operations. Some CassandraTemplate methods (such as select(Statement<?>, Class<T>)) also accepts CQL Statement objects as part of their API.

It’s possible to participate in prepared statements when calling methods accepting a Statement with a SimpleStatement object. The template API extracts the query string and parameters (positional and named parameters) and uses these to prepare, bind, and run the statement. Non-SimpleStatement objects cannot be used with prepared statements.

7.11.3. Caching Prepared Statements

Since Cassandra driver 4.0, prepared statements are cached by the CqlSession cache so it is okay to prepare the same string twice. Previous versions required caching of prepared statements outside of the driver. See also the Driver documentation on Prepared Statements for further reference.

8. Reactive Cassandra Support

The reactive Cassandra support contains a wide range of features:

  • Spring configuration support using Java-based @Configuration classes.

  • ReactiveCqlTemplate helper class that increases productivity by properly handling common Cassandra data access operations.

  • ReactiveCassandraTemplate helper class that increases productivity by using ReactiveCassandraOperations in a reactive manner. It includes integrated object mapping between tables and POJOs.

  • Exception translation into Spring’s portable Data Access Exception Hierarchy.

  • Feature rich object mapping integrated with Spring’s Conversion Service.

  • Java-based Query, Criteria, and Update DSLs.

  • Automatic implementation of Repository interfaces, including support for custom finder methods.

For most data-oriented tasks, you can use the ReactiveCassandraTemplate or the repository support, which use the rich object mapping functionality. ReactiveCqlTemplate is commonly used to increment counters or perform ad-hoc CRUD operations. ReactiveCqlTemplate also provides callback methods that make it easy to get low-level API objects, such as com.datastax.oss.driver.api.core.CqlSession, which let you communicate directly with Cassandra. Spring Data for Apache Cassandra uses consistent naming conventions on objects in various APIs to those found in the DataStax Java Driver so that they are immediately familiar and so that you can map your existing knowledge onto the Spring APIs.

8.1. Getting Started

Spring Data for Apache Cassandra requires Apache Cassandra 2.1 or later and Datastax Java Driver 4.0 or later. An easy way to quickly set up and bootstrap a working environment is to create a Spring-based project in STS or use Spring Initializer.

First, you need to set up a running Apache Cassandra server. See the Apache Cassandra Quick Start Guide for an explanation on how to start Apache Cassandra. Once installed, starting Cassandra is typically a matter of running the following command: CASSANDRA_HOME/bin/cassandra -f.

To create a Spring project in STS, go to File → New → Spring Template Project → Simple Spring Utility Project and press Yes when prompted. Then enter a project and a package name, such as org.spring.data.cassandra.example.

Then you can add the following dependency declaration to your pom.xml file’s dependencies section.

<dependencies>

  <dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-cassandra</artifactId>
    <version>3.3.0-SNAPSHOT</version>
  </dependency>

</dependencies>

Also, you should change the version of Spring in the pom.xml file to be as follows:

<spring.framework.version>5.3.10</spring.framework.version>

If using a milestone release instead of a GA release, you also need to add the location of the Spring Milestone repository for Maven to your pom.xml file so that it is at the same level of your <dependencies/> element, as follows:

<repositories>
  <repository>
    <id>spring-milestone</id>
    <name>Spring Maven MILESTONE Repository</name>
    <url>https://repo.spring.io/libs-milestone</url>
  </repository>
</repositories>

The repository is also browseable here.

You can also browse all Spring repositories here.

Now you can create a simple Java application that stores and reads a domain object to and from Cassandra.

To do so, first create a simple domain object class to persist, as the following example shows:

package org.springframework.data.cassandra.example;

import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.Table;

@Table
public class Person {

  @PrimaryKey private final String id;

  private final String name;
  private final int age;

  public Person(String id, String name, int age) {
    this.id = id;
    this.name = name;
    this.age = age;
  }

  public String getId() {
    return id;
  }

  private String getName() {
    return name;
  }

  private int getAge() {
    return age;
  }

  @Override
  public String toString() {
    return String.format("{ @type = %1$s, id = %2$s, name = %3$s, age = %4$d }", getClass().getName(), getId(),
        getName(), getAge());
  }
}

Next, create the main application to run, as the following example shows:

package org.springframework.data.cassandra.example;

import reactor.core.publisher.Mono;

import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
import org.springframework.data.cassandra.core.ReactiveCassandraTemplate;
import org.springframework.data.cassandra.core.cql.session.DefaultBridgedReactiveSession;
import org.springframework.data.cassandra.core.query.Criteria;
import org.springframework.data.cassandra.core.query.Query;

import com.datastax.oss.driver.api.core.CqlSession;

public class ReactiveCassandraApplication {

  private static final Logger LOGGER = LoggerFactory.getLogger(ReactiveCassandraApplication.class);

  private static Person newPerson(String name, int age) {
    return new Person(UUID.randomUUID().toString(), name, age);
  }

  public static void main(String[] args) {

    CqlSession cqlSession = CqlSession.builder().withKeyspace("mykeyspace").build();

     ReactiveCassandraOperations template = new ReactiveCassandraTemplate(new DefaultBridgedReactiveSession(cqlSession));

    Mono<Person> jonDoe = template.insert(newPerson("Jon Doe", 40));

    jonDoe.flatMap(it -> template.selectOne(Query.query(Criteria.where("id").is(it.getId())), Person.class))
        .doOnNext(it -> LOGGER.info(it.toString()))
        .then(template.truncate(Person.class))
        .block();

    cqlSession.close();
  }

}

Even in this simple example, there are a few notable things to point out:

  • A fully synchronous flow does not benefit from a reactive infrastructure, because a reactive programming model requires synchronization.

  • You can create an instance of ReactiveCassandraTemplate with a Cassandra CqlSession.

  • You must annotate your POJO as a Cassandra @Table and annotate the @PrimaryKey. Optionally, you can override these mapping names to match your Cassandra database table and column names.

  • You can either use raw CQL or the DataStax QueryBuilder API to construct your queries.

8.2. Examples Repository

A Github repository contains several examples that you can download and play around with to get a feel for how the library works.

8.3. Connecting to Cassandra with Spring

One of the first tasks when using Apache Cassandra with Spring is to create a com.datastax.oss.driver.api.core.CqlSession object by using the Spring IoC container. You can do so either by using Java-based bean metadata or by using XML-based bean metadata. These are discussed in the following sections.

For those not familiar with how to configure the Spring container using Java-based bean metadata instead of XML-based metadata, see the high-level introduction in the reference docs here as well as the detailed documentation here.

8.3.1. Registering a Session instance using Java-based metadata

You can configure Reactive Cassandra support by using Java Configuration classes. Reactive Cassandra support adapts a CqlSession to provide a reactive processing model on top of an asynchronous driver.

A reactive CqlSession is configured similarly to an imperative CqlSession. We provide supporting configuration classes that come with predefined defaults and require only environment-specific information to configure Spring Data for Apache Cassandra. The base class for reactive support is AbstractReactiveCassandraConfiguration. This configuration class extends the imperative AbstractCassandraConfiguration, so the reactive support also configures the imperative API support. The following example shows how to register Apache Cassandra beans in a configuration class: ReactiveAppCassandraConfiguration .Registering Spring Data for Apache Cassandra beans using AbstractReactiveCassandraConfiguration

@Configuration
public class ReactiveCassandraConfiguration extends AbstractReactiveCassandraConfiguration {

  /*
   * Provide a contact point to the configuration.
   */
  public String getContactPoints() {
    return "localhost";
  }

  /*
   * Provide a keyspace name to the configuration.
   */
  public String getKeyspaceName() {
    return "mykeyspace";
  }
}

The configuration class in the preceding example is schema-management-enabled to create CQL objects during startup. See Schema Management for further details.

8.4. ReactiveCqlTemplate

The ReactiveCqlTemplate class is the central class in the core CQL package. It handles the creation and release of resources. It performs the basic tasks of the core CQL workflow, such as creating and running statements, leaving application code to provide CQL and extract results. The ReactiveCqlTemplate class runs CQL queries and update statements and performs iteration over ResultSet instances and extraction of returned parameter values. It also catches CQL exceptions and translates them into the generic, more informative, exception hierarchy defined in the org.springframework.dao package.

When you use the ReactiveCqlTemplate in your code, you need only implement callback interfaces, which have a clearly defined contract. Given a Connection, the ReactivePreparedStatementCreator callback interface creates a prepared statement with the provided CQL and any necessary parameter arguments. The RowCallbackHandler interface extracts values from each row of a ReactiveResultSet.

The ReactiveCqlTemplate can be used within a DAO implementation through direct instantiation with a ReactiveSessionFactory reference or be configured in the Spring container and given to DAOs as a bean reference. ReactiveCqlTemplate is a foundational building block for ReactiveCassandraTemplate.

All CQL issued by this class is logged at the DEBUG level under the category corresponding to the fully-qualified class name of the template instance (typically ReactiveCqlTemplate, but it may be different if you use a custom subclass of the ReactiveCqlTemplate class).

8.4.1. Examples of ReactiveCqlTemplate Class Usage

This section provides some examples of ReactiveCqlTemplate class usage. These examples are not an exhaustive list of all of the functionality exposed by the ReactiveCqlTemplate. See the attendant Javadocs for that.

Querying (SELECT) with ReactiveCqlTemplate

The following query gets the number of rows in a relation:

Mono<Integer> rowCount = reactiveCqlTemplate.queryForObject("SELECT COUNT(*) FROM t_actor", Integer.class);

The following query uses a bind variable:

Mono<Integer> countOfActorsNamedJoe = reactiveCqlTemplate.queryForObject(
  "SELECT COUNT(*) FROM t_actor WHERE first_name = ?", Integer.class, "Joe");

The following example queries for a String:

Mono<String> lastName = reactiveCqlTemplate.queryForObject(
  "SELECT last_name FROM t_actor WHERE id = ?",
  String.class, 1212L);

The following example queries and populates a single domain object:

Mono<Actor> actor = reactiveCqlTemplate.queryForObject(
  "SELECT first_name, last_name FROM t_actor WHERE id = ?",
  new RowMapper<Actor>() {
    public Actor mapRow(Row row, int rowNum) {
      Actor actor = new Actor();
      actor.setFirstName(row.getString("first_name"));
      actor.setLastName(row.getString("last_name"));
      return actor;
    }},
  1212L);

The following example queries and populates a number of domain objects:

Flux<Actor> actors = reactiveCqlTemplate.query(
"SELECT first_name, last_name FROM t_actor",
  new RowMapper<Actor>() {
    public Actor mapRow(Row row, int rowNum) {
      Actor actor = new Actor();
      actor.setFirstName(row.getString("first_name"));
      actor.setLastName(row.getString("last_name"));
      return actor;
    }
});

If the last two snippets of code actually existed in the same application, it would make sense to remove the duplication present in the two RowMapper anonymous inner classes and extract them into a single class (typically a static nested class) that can then be referenced by DAO methods as needed.

For example, it might be better to write the last code snippet as follows:

Flux<Actor> findAllActors() {
  return reactiveCqlTemplate.query("SELECT first_name, last_name FROM t_actor", ActorMapper.INSTANCE);
}

enum ActorMapper implements RowMapper<Actor> {

  INSTANCE;

  public Actor mapRow(Row row, int rowNum) {
    Actor actor = new Actor();
    actor.setFirstName(row.getString("first_name"));
    actor.setLastName(row.getString("last_name"));
    return actor;
  }
}
INSERT, UPDATE, and DELETE with ReactiveCqlTemplate

You can use the execute(…) method to perform INSERT, UPDATE, and DELETE operations. Parameter values are usually provided as variable arguments or, alternatively, as an object array.

The following example shows how to perform an INSERT operation with ReactiveCqlTemplate:

Mono<Boolean> applied = reactiveCqlTemplate.execute(
  "INSERT INTO t_actor (first_name, last_name) VALUES (?, ?)",
  "Leonor", "Watling");

The following example shows how to perform an UPDATE operation with ReactiveCqlTemplate:

Mono<Boolean> applied = reactiveCqlTemplate.execute(
  "UPDATE t_actor SET last_name = ? WHERE id = ?",
  "Banjo", 5276L);

The following example shows how to perform an DELETE operation with ReactiveCqlTemplate:

Mono<Boolean> applied = reactiveCqlTemplate.execute(
  "DELETE FROM actor WHERE id = ?",
  actorId);

8.5. Exception Translation

The Spring Framework provides exception translation for a wide variety of database and mapping technologies. This has traditionally been for JDBC and JPA. Spring Data for Apache Cassandra extends this feature to Apache Cassandra by providing an implementation of the org.springframework.dao.support.PersistenceExceptionTranslator interface.

The motivation behind mapping to Spring’s consistent data access exception hierarchy is to let you write portable and descriptive exception handling code without resorting to coding against and handling specific Cassandra exceptions. All of Spring’s data access exceptions are inherited from the DataAccessException class, so you can be sure that you can catch all database-related exceptions within a single try-catch block.

ReactiveCqlTemplate and ReactiveCassandraTemplate propagate exceptions as early as possible. Exceptions that occur during the processing of the reactive sequence are emitted as error signals.

8.6. Introduction to ReactiveCassandraTemplate

The ReactiveCassandraTemplate class, located in the org.springframework.data.cassandra package, is the central class in Spring Data’s Cassandra support. It provides a rich feature set to interact with the database. The template offers convenience data access operations to create, update, delete, and query Cassandra and provides a mapping between your domain objects and Cassandra table rows.

Once configured, ReactiveCassandraTemplate is thread-safe and can be reused across multiple instances.

The mapping between rows in a Cassandra table and domain classes is done by delegating to an implementation of the CassandraConverter interface. Spring provides a default implementation, MappingCassandraConverter, but you can also write your own custom converter. See “Mapping” for more detailed information.

The ReactiveCassandraTemplate class implements the ReactiveCassandraOperations interface. As often as possible, the methods names ReactiveCassandraOperations match names in Cassandra to make the API familiar to developers who are familiar with Cassandra.

For example, you can find methods such as select, insert, delete, and update. The design goal was to make it as easy as possible to transition between the use of the base Cassandra driver and ReactiveCassandraOperations. A major difference between the two APIs is that ReactiveCassandraOperations can be passed domain objects instead of CQL and query objects.

The preferred way to reference operations on a ReactiveCassandraTemplate instance is through its interface, ReactiveCassandraOperations.

The default converter implementation for ReactiveCassandraTemplate is MappingCassandraConverter. While the MappingCassandraConverter can make use of additional metadata to specify the mapping of objects to rows, it can also convert objects that contain no additional metadata by using conventions for the mapping of fields and table names. These conventions, as well as the use of mapping annotations, are explained in “Mapping”.

Another central feature of CassandraTemplate is exception translation. Exceptions thrown by the Cassandra Java driver are translated into Spring’s portable Data Access Exception hierarchy. See “Exception Translation” for more information.

8.6.1. Instantiating ReactiveCassandraTemplate

ReactiveCassandraTemplate should always be configured as a Spring bean, although an earlier example showed how to instantiate it directly. However, this section assumes that the template is used in a Spring module, so it also assumes that the Spring container is being used.

There are two ways to get a ReactiveCassandraTemplate, depending on how you load you Spring ApplicationContext:

Autowiring

You can autowire a ReactiveCassandraTemplate into your project, as the following example shows:

@Autowired
private ReactiveCassandraOperations reactiveCassandraOperations;

Like all Spring autowiring, this assumes there is only one bean of type ReactiveCassandraOperations in the ApplicationContext. If you have multiple ReactiveCassandraTemplate beans (which can be the case if you are working with multiple keyspaces in the same project), then you can use the @Qualifier annotation to designate which bean you want to autowire.

@Autowired
@Qualifier("keyspaceTwoTemplateBeanId")
private ReactiveCassandraOperations reactiveCassandraOperations;
Bean Lookup with ApplicationContext

You can also look up the ReactiveCassandraTemplate bean from the ApplicationContext, as shown in the following example:

ReactiveCassandraOperations reactiveCassandraOperations = applicationContext.getBean("reactiveCassandraOperations", ReactiveCassandraOperations.class);

8.7. Saving, Updating, and Removing Rows

ReactiveCassandraTemplate provides a simple way for you to save, update, and delete your domain objects and map those objects to tables managed in Cassandra.

8.7.1. Methods for Inserting and Updating rows

CassandraTemplate has several convenient methods for saving and inserting your objects. To have more fine-grained control over the conversion process, you can register Spring Converter instances with the MappingCassandraConverter (for example, Converter<Row, Person>).

The difference between insert and update operations is that INSERT operations do not insert null values.

The simple case of using the INSERT operation is to save a POJO. In this case, the table name is determined by the simple class name (not the fully qualified class name). The table to store the object can be overridden by using mapping metadata.

When inserting or updating, the id property must be set. Apache Cassandra has no means to generate an ID.

The following example uses the save operation and retrieves its contents:

Example 14. Inserting and retrieving objects by using the CassandraTemplate
import static org.springframework.data.cassandra.core.query.Criteria.where;
import static org.springframework.data.cassandra.core.query.Query.query;
…

Person bob = new Person("Bob", 33);
cassandraTemplate.insert(bob);

Mono<Person> queriedBob = reactiveCassandraTemplate.selectOneById(query(where("age").is(33)), Person.class);

You can use the following operations to insert and save:

  • void insert (Object objectToSave): Inserts the object in an Apache Cassandra table.

  • WriteResult insert (Object objectToSave, InsertOptions options): Inserts the object in an Apache Cassandra table and applies InsertOptions.

You can use the following update operations:

  • void update (Object objectToSave): Updates the object in an Apache Cassandra table.

  • WriteResult update (Object objectToSave, UpdateOptions options): Updates the object in an Apache Cassandra table and applies UpdateOptions.

You can also use the old fashioned way and write your own CQL statements, as the following example shows:

String cql = "INSERT INTO person (age, name) VALUES (39, 'Bob')";

Mono<Boolean> applied = reactiveCassandraTemplate.getReactiveCqlOperations().execute(cql);

You can also configure additional options such as TTL, consistency level, and lightweight transactions when using InsertOptions and UpdateOptions.

Which Table Are My Rows Inserted into?

You can manage the table name that is used for operating on the tables in two ways. The default table name is the simple class name changed to start with a lower-case letter. So, an instance of the com.example.Person class would be stored in the person table. The second way is to specify a table name in the @Table annotation.

8.7.2. Updating Rows in a Table

For updates, you can select to update a number of rows.

The following example shows updating a single account object by adding a one-time $50.00 bonus to the balance with the + assignment:

Example 15. Updating rows using ReactiveCasandraTemplate
import static org.springframework.data.cassandra.core.query.Criteria.where;
import org.springframework.data.cassandra.core.query.Query;
import org.springframework.data.cassandra.core.query.Update;

…

Mono<Boolean> wasApplied = reactiveCassandraTemplate.update(Query.query(where("id").is("foo")),
  Update.create().increment("balance", 50.00), Account.class);

In addition to the Query discussed earlier, we provide the update definition by using an Update object. The Update class has methods that match the update assignments available for Apache Cassandra.

Most methods return the Update object to provide a fluent API for code styling purposes.

For more detail, see “Methods for Executing Updates for Rows”.

9. Cassandra Repositories

This chapter covers the details of the Spring Data Repository support for Apache Cassandra. Cassandra’s repository support builds on the core repository support explained in “[repositories]”. Cassandra repositories use CassandraTemplate and its wired CqlTemplate as infrastructure beans. You should understand the basic concepts explained there before proceeding.

9.1. Usage

To access domain entities stored in Apache Cassandra, you can use Spring Data’s sophisticated repository support, which significantly eases implementing DAOs. To do so, create an interface for your repository, as the following example shows:

Example 16. Sample Person entity
@Table
public class Person {

  @Id
  private String id;
  private String firstname;
  private String lastname;

  // … getters and setters omitted
}

Note that the entity has a property named id of type String. The default serialization mechanism used in CassandraTemplate (which backs the repository support) regards properties named id as being the row ID.

The following example shows a repository definition to persist Person entities:

Example 17. Basic repository interface to persist Person entities
public interface PersonRepository extends CrudRepository<Person, String> {

  // additional custom finder methods go here
}

Right now, the interface in the preceding example serves only typing purposes, but we add additional methods to it later.

Next, in your Spring configuration, add the following (if you use Java for configuration):

If you want to use Java configuration, use the @EnableCassandraRepositories annotation. The annotation carries the same attributes as the namespace element. If no base package is configured, the infrastructure scans the package of the annotated configuration class. The following example shows how to use the @EnableCassandraRepositories annotation:

Example 18. Java configuration for repositories
@Configuration
@EnableCassandraRepositories
class ApplicationConfig extends AbstractCassandraConfiguration {

  @Override
  protected String getKeyspaceName() {
    return "keyspace";
  }

  public String[] getEntityBasePackages() {
    return new String[] { "com.oreilly.springdata.cassandra" };
  }
}

If you want to use XML configuration, then the following example shows a minimal configuration snippet:

Example 19. Cassandra repository Spring XML configuration
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:cassandra="http://www.springframework.org/schema/data/cassandra"
  xsi:schemaLocation="
    http://www.springframework.org/schema/data/cassandra
    https://www.springframework.org/schema/data/cassandra/spring-cassandra.xsd
    http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd">

    <cassandra:session port="9042" keyspace-name="keyspaceName"/>

    <cassandra:mapping
            entity-base-packages="com.acme.*.entities">
    </cassandra:mapping>

    <cassandra:converter/>

    <cassandra:template/>

    <cassandra:repositories base-package="com.acme.*.entities"/>
</beans>

The cassandra:repositories namespace element causes the base packages to be scanned for interfaces that extend CrudRepository and create Spring beans for each one found. By default, the repositories are wired with a CassandraTemplate Spring bean called cassandraTemplate, so you only need to configure cassandra-template-ref explicitly if you deviate from this convention.

Because our domain repository extends CrudRepository, it provides you with basic CRUD operations. Working with the repository instance is a matter of injecting the repository as a dependency into a client, as the following example does by autowiring PersonRepository:

Example 20. Basic access to Person entities
@RunWith(SpringRunner.class)
@ContextConfiguration
public class PersonRepositoryTests {

    @Autowired PersonRepository repository;

    @Test
    public void readsPersonTableCorrectly() {

      List<Person> persons = repository.findAll();
      assertThat(persons.isEmpty()).isFalse();
    }
}

Cassandra repositories support paging and sorting for paginated and sorted access to the entities. Cassandra paging requires a paging state to forward-only navigate through pages. A Slice keeps track of the current paging state and allows for creation of a Pageable to request the next page. The following example shows how to set up paging access to Person entities:

Example 21. Paging access to Person entities
@RunWith(SpringRunner.class)
@ContextConfiguration
public class PersonRepositoryTests {

    @Autowired PersonRepository repository;

    @Test
    public void readsPagesCorrectly() {

      Slice<Person> firstBatch = repository.findAll(CassandraPageRequest.first(10));

      assertThat(firstBatch).hasSize(10);

      Page<Person> nextBatch = repository.findAll(firstBatch.nextPageable());

      // …
    }
}
Cassandra repositories do not extend PagingAndSortingRepository, because classic paging patterns using limit/offset are not applicable to Cassandra.

The preceding example creates an application context with Spring’s unit test support, which performs annotation-based dependency injection into the test class. Inside the test cases (the test methods), we use the repository to query the data store. We invoke the repository query method that requests all Person instances.

9.2. Query Methods

Most of the data access operations you usually trigger on a repository result in a query being executed against the Apache Cassandra database. Defining such a query is a matter of declaring a method on the repository interface. The following example shows a number of such method declarations:

Example 22. PersonRepository with query methods
public interface PersonRepository extends CrudRepository<Person, String> {

    List<Person> findByLastname(String lastname);                           (1)

    Slice<Person> findByFirstname(String firstname, Pageable pageRequest);  (2)

    List<Person> findByFirstname(String firstname, QueryOptions opts);      (3)

    List<Person> findByFirstname(String firstname, Sort sort);              (4)

    Person findByShippingAddress(Address address);                          (5)

    Person findFirstByShippingAddress(Address address);                     (6)

    Stream<Person> findAllBy();                                             (7)

    @AllowFiltering
    List<Person> findAllByAge(int age);                                     (8)
}
1 The method shows a query for all people with the given lastname. The query is derived from parsing the method name for constraints, which can be concatenated with And. Thus, the method name results in a query expression of SELECT * FROM person WHERE lastname = 'lastname'.
2 Applies pagination to a query. You can equip your method signature with a Pageable parameter and let the method return a Slice instance, and we automatically page the query accordingly.
3 Passing a QueryOptions object applies the query options to the resulting query before its execution.
4 Applies dynamic sorting to a query. You can add a Sort parameter to your method signature, and Spring Data automatically applies ordering to the query.
5 Shows that you can query based on properties that are not a primitive type by using Converter instances registered in CustomConversions. Throws IncorrectResultSizeDataAccessException if more than one match is found.
6 Uses the First keyword to restrict the query to only the first result. Unlike the preceding method, this method does not throw an exception if more than one match is found.
7 Uses a Java 8 Stream to read and convert individual elements while iterating the stream.
8 Shows a query method annotated with @AllowFiltering, to allow server-side filtering.
Querying non-primary key properties requires secondary indexes.

The following table shows short examples of the keywords that you can use in query methods:

Table 2. Supported keywords for query methods
Keyword Sample Logical result

After

findByBirthdateAfter(Date date)

birthdate > date

GreaterThan

findByAgeGreaterThan(int age)

age > age

GreaterThanEqual

findByAgeGreaterThanEqual(int age)

age >= age

Before

findByBirthdateBefore(Date date)

birthdate < date

LessThan

findByAgeLessThan(int age)

age < age

LessThanEqual

findByAgeLessThanEqual(int age)

age ⇐ age

Between

findByAgeBetween(int from, int to) and findByAgeBetween(Range<Integer> range)

age > from AND age < to and lower / upper bounds (> / >= & < / ) according to Range

In

findByAgeIn(Collection ages)

age IN (ages…​)

Like, StartingWith, EndingWith

findByFirstnameLike(String name)

firstname LIKE (name as like expression)

Containing on String

findByFirstnameContaining(String name)

firstname LIKE (name as like expression)

Containing on Collection

findByAddressesContaining(Address address)

addresses CONTAINING address

(No keyword)

findByFirstname(String name)

firstname = name

IsTrue, True

findByActiveIsTrue()

active = true

IsFalse, False

findByActiveIsFalse()

active = false

9.3. Repository Delete Queries

The keywords in the preceding table can be used in conjunction with delete…By to create queries that delete matching documents.

interface PersonRepository extends Repository<Person, String> {

  void deleteWithoutResultByLastname(String lastname);

  boolean deleteByLastname(String lastname);
}

Delete queries return whether the query was applied or terminate without returning a value using void.

Unresolved directive in reference/cassandra-repositories.adoc - include::../../../../../../spring-data-commons/src/main/asciidoc/repository-projections.adoc[leveloffset=+2]

9.3.1. Query Options

You can specify query options for query methods by passing a QueryOptions object. The options apply to the query before the actual query execution. QueryOptions is treated as a non-query parameter and is not considered to be a query parameter value. Query options apply to derived and string @Query repository methods.

To statically set the consistency level, use the @Consistency annotation on query methods. The declared consistency level is applied to the query each time it is executed. The following example sets the consistency level to ConsistencyLevel.LOCAL_ONE:

public interface PersonRepository extends CrudRepository<Person, String> {

    @Consistency(ConsistencyLevel.LOCAL_ONE)
    List<Person> findByLastname(String lastname);

    List<Person> findByFirstname(String firstname, QueryOptions options);
}

The DataStax Cassandra documentation includes a good discussion of the available consistency levels.

You can control fetch size, consistency level, and retry policy defaults by configuring the following parameters on the CQL API instances: CqlTemplate, AsyncCqlTemplate, and ReactiveCqlTemplate. Defaults apply if the particular query option is not set.

9.3.2. CDI Integration

Instances of the repository interfaces are usually created by a container, and the Spring container is the most natural choice when working with Spring Data. Spring Data for Apache Cassandra ships with a custom CDI extension that allows using the repository abstraction in CDI environments. The extension is part of the JAR. To activate it, drop the Spring Data for Apache Cassandra JAR into your classpath. You can now set up the infrastructure by implementing a CDI Producer for the CassandraTemplate, as the following examlpe shows:

class CassandraTemplateProducer {

  @Produces
  @Singleton
  public CqlSession createSession() {
    return CqlSession.builder().withKeyspace("my-keyspace").build();
  }

  @Produces
  @ApplicationScoped
  public CassandraOperations createCassandraOperations(CqlSession session) throws Exception {

    CassandraMappingContext mappingContext = new CassandraMappingContext();
    mappingContext.setUserTypeResolver(new SimpleUserTypeResolver(session));
    mappingContext.afterPropertiesSet();

    MappingCassandraConverter cassandraConverter = new MappingCassandraConverter(mappingContext);
    cassandraConverter.afterPropertiesSet();

    return new CassandraAdminTemplate(session, cassandraConverter);
  }

  public void close(@Disposes CqlSession session) {
    session.close();
  }
}

The Spring Data for Apache Cassandra CDI extension picks up CassandraOperations as a CDI bean and creates a proxy for a Spring Data repository whenever a bean of a repository type is requested by the container. Thus, obtaining an instance of a Spring Data repository is a matter of declaring an injected property, as the following example shows:

class RepositoryClient {

  @Inject PersonRepository repository;

  public void businessMethod() {
    List<Person> people = repository.findAll();
  }
}

10. Reactive Cassandra Repositories

This chapter outlines the specialties handled by the reactive repository support for Apache Cassandra. It builds on the core repository infrastructure explained in Cassandra Repositories, so you should have a good understanding of the basic concepts explained there.

Cassandra repositories use ReactiveCassandraTemplate and its wired ReactiveCqlTemplate as infrastructure beans.

Reactive usage is broken up into two phases: Composition and Execution.

Calling repository methods lets you compose a reactive sequence by obtaining Publisher instances and applying operators. No I/O happens until you subscribe. Passing the reactive sequence to a reactive execution infrastructure, such as Spring WebFlux or Vert.x), subscribes to the publisher and initiate the actual execution. See the Project reactor documentation for more detail.

10.1. Reactive Composition Libraries

The reactive space offers various reactive composition libraries. The most common libraries are RxJava and Project Reactor.

Spring Data for Apache Cassandra is built on top of the DataStax Cassandra Driver. The driver is not reactive but the asynchronous capabilities allow us to adopt and expose the Publisher APIs to provide maximum interoperability by relying on the Reactive Streams initiative. Static APIs, such as ReactiveCassandraOperations, are provided by using Project Reactor’s Flux and Mono types. Project Reactor offers various adapters to convert reactive wrapper types (Flux to Observable and back), but conversion can easily clutter your code.

Spring Data’s repository abstraction is a dynamic API that is mostly defined by you and your requirements as you declare query methods. Reactive Cassandra repositories can be implemented by using either RxJava or Project Reactor wrapper types by extending from one of the library-specific repository interfaces:

  • ReactiveCrudRepository

  • ReactiveSortingRepository

  • RxJava2CrudRepository

  • RxJava2SortingRepository

Spring Data converts reactive wrapper types behind the scenes so that you can stick to your favorite composition library.

10.2. Usage

To access domain entities stored in Apache Cassandra, you can use Spring Data’s sophisticated repository support, which significantly eases implementing DAOs. To do so, create an interface for your repository, as the following example shows:

Example 23. Sample Person entity
@Table
public class Person {

  @Id
  private String id;
  private String firstname;
  private String lastname;

  // … getters and setters omitted
}

Note that the entity has a property named id of type String. The default serialization mechanism used in CassandraTemplate (which backs the repository support) regards properties named id as being the row ID.

The following example shows a repository definition to persist Person entities:

Example 24. Basic repository interface to persist Person entities
public interface ReactivePersonRepository extends ReactiveSortingRepository<Person, Long> {

  Flux<Person> findByFirstname(String firstname);                                (1)

  Flux<Person> findByFirstname(Publisher<String> firstname);                     (2)

  Mono<Person> findByFirstnameAndLastname(String firstname, String lastname);    (3)

  Mono<Person> findFirstByFirstname(String firstname);                           (4)

  @AllowFiltering
  Flux<Person> findByAge(int age);                                               (5)
}
1 A query for all people with the given firstname. The query is derived by parsing the method name for constraints, which can be concatenated with And and Or. Thus, the method name results in a query expression of SELECT * FROM person WHERE firstname = :firstname.
2 A query for all people with the given firstname once the firstname is emitted from the given Publisher.
3 Find a single entity for the given criteria. Completes with IncorrectResultSizeDataAccessException on non-unique results.
4 Unlike the preceding query, the first entity is always emitted even if the query yields more result rows.
5 A query method annotated with @AllowFiltering, which allows server-side filtering.

For Java configuration, use the @EnableReactiveCassandraRepositories annotation. The annotation carries the same attributes as the corresponding XML namespace element. If no base package is configured, the infrastructure scans the package of the annotated configuration class. The following example uses the @EnableReactiveCassandraRepositories annotation:

Example 25. Java configuration for repositories
@Configuration
@EnableReactiveCassandraRepositories
class ApplicationConfig extends AbstractReactiveCassandraConfiguration {

  @Override
  protected String getKeyspaceName() {
    return "keyspace";
  }

  public String[] getEntityBasePackages() {
    return new String[] { "com.oreilly.springdata.cassandra" };
  }
}

Since our domain repository extends ReactiveSortingRepository, it provides you with CRUD operations as well as methods for sorted access to the entities. Working with the repository instance is a matter of dependency injecting it into a client, as the following example shows:

Example 26. Sorted access to Person entities
public class PersonRepositoryTests {

    @Autowired ReactivePersonRepository repository;

    @Test
    public void sortsElementsCorrectly() {
        Flux<Person> people = repository.findAll(Sort.by(new Order(ASC, "lastname")));
    }
}

Cassandra repositories support paging and sorting for paginated and sorted access to the entities. Cassandra paging requires a paging state to forward-only navigate through pages. A Slice keeps track of the current paging state and allows for creation of a Pageable to request the next page. The following example shows how to set up paging access to Person entities:

Example 27. Paging access to Person entities
@RunWith(SpringRunner.class)
@ContextConfiguration
public class PersonRepositoryTests {

    @Autowired PersonRepository repository;

    @Test
    public void readsPagesCorrectly() {

      Mono<Slice<Person>> firstBatch = repository.findAll(CassandraPageRequest.first(10));

      Mono<Slice<Person>> nextBatch = firstBatch.flatMap(it -> repository.findAll(it.nextPageable()));

      // …
    }
}

The preceding example creates an application context with Spring’s unit test support, which performs annotation-based dependency injection into the test class. Inside the test cases (the test methods), we use the repository to query the data store. We invoke the repository query method that requests all Person instances.

10.3. Features

Spring Data’s Reactive Cassandra support comes with the same set of features as the support for imperative repositories.

It supports the following features:

Query methods must return a reactive type. Resolved types (User versus Mono<User>) are not supported.

Unresolved directive in index.adoc - include::../../../../../spring-data-commons/src/main/asciidoc/auditing.adoc[leveloffset=+1] :leveloffset: +1

11. General Auditing Configuration for Cassandra

To activate auditing functionality, add the Spring Data for Apache Cassandra auditing namespace element to your configuration, as the following example shows:

Example 28. Activating auditing by using XML configuration
<cassandra:auditing mapping-context-ref="customMappingContext" auditor-aware-ref="yourAuditorAwareImpl"/>

Alternatively, auditing can be enabled by annotating a configuration class with the @EnableCassandraAuditing annotation, as the following example shows:

Example 29. Activating auditing using JavaConfig
@Configuration
@EnableCassandraAuditing
class Config {

  @Bean
  public AuditorAware<AuditableUser> myAuditorProvider() {
      return new AuditorAwareImpl();
  }
}

If you expose a bean of type AuditorAware to the ApplicationContext, the auditing infrastructure picks it up automatically and uses it to determine the current user to be set on domain types. If you have multiple implementations registered in the ApplicationContext, you can select the one to be used by explicitly setting the auditorAwareRef attribute of @EnableCassandraAuditing.

To enable auditing, leveraging a reactive programming model, use the @EnableReactiveCassandraAuditing annotation.
If you expose a bean of type ReactiveAuditorAware to the ApplicationContext, the auditing infrastructure picks it up automatically and uses it to determine the current user to be set on domain types. If you have multiple implementations registered in the ApplicationContext, you can select the one to be used by explicitly setting the auditorAwareRef attribute of @EnableReactiveCassandraAuditing.

Example 30. Activating reactive auditing using JavaConfig
@Configuration
@EnableReactiveCassandraAuditing
class Config {

  @Bean
  public ReactiveAuditorAware<AuditableUser> myAuditorProvider() {
      return new AuditorAwareImpl();
  }
}

12. Mapping

Rich object mapping support is provided by the MappingCassandraConverter. MappingCassandraConverter has a rich metadata model that provides a complete feature set of functionality to map domain objects to CQL tables.

The mapping metadata model is populated by using annotations on your domain objects. However, the infrastructure is not limited to using annotations as the only source of metadata. The MappingCassandraConverter also lets you map domain objects to tables without providing any additional metadata, by following a set of conventions.

In this chapter, we describe the features of the MappingCassandraConverter, how to use conventions for mapping domain objects to tables, and how to override those conventions with annotation-based mapping metadata.

Unresolved directive in reference/mapping.adoc - include::../../../../../../spring-data-commons/src/main/asciidoc/object-mapping.adoc[leveloffset=+1]

12.1. Data Mapping and Type Conversion

This section explains how types are mapped to and from an Apache Cassandra representation.

Spring Data for Apache Cassandra supports several types that are provided by Apache Cassandra. In addition to these types, Spring Data for Apache Cassandra provides a set of built-in converters to map additional types. You can provide your own custom converters to adjust type conversion. See “[cassandra.mapping.explicit-converters]” for further details. The following table maps Spring Data types to Cassandra types:

Table 3. Type
Type Cassandra types

String

text (default), varchar, ascii

double, Double

double

float, Float

float

long, Long

bigint (default), counter

int, Integer

int

short, Short

smallint

byte, Byte

tinyint

boolean, Boolean

boolean

BigInteger

varint

BigDecimal

decimal

java.util.Date

timestamp

com.datastax.driver.core.LocalDate

date

InetAddress

inet

ByteBuffer

blob

java.util.UUID

uuid

TupleValue, mapped Tuple Types

tuple<…>

UDTValue, mapped User-Defined Types

user type

java.util.Map<K, V>

map

java.util.List<E>

list

java.util.Set<E>

set

Enum

text (default), bigint, varint, int, smallint, tinyint

LocalDate
(Joda, Java 8, JSR310-BackPort)

date

LocalTime+ (Joda, Java 8, JSR310-BackPort)

time

LocalDateTime, LocalTime, Instant
(Joda, Java 8, JSR310-BackPort)

timestamp

ZoneId (Java 8, JSR310-BackPort)

text

Each supported type maps to a default Cassandra data type. Java types can be mapped to other Cassandra types by using @CassandraType, as the following example shows:

Example 31. Enum mapping to numeric types
@Table
public class EnumToOrdinalMapping {

  @PrimaryKey String id;

  @CassandraType(type = Name.INT) Condition asOrdinal;
}

public enum Condition {
  NEW, USED
}

12.2. Convention-based Mapping

MappingCassandraConverter uses a few conventions for mapping domain objects to CQL tables when no additional mapping metadata is provided. The conventions are:

  • The simple (short) Java class name is mapped to the table name by being changed to lower case. For example, com.bigbank.SavingsAccount maps to a table named savingsaccount.

  • The converter uses any registered Spring Converter instances to override the default mapping of object properties to tables fields.

  • The properties of an object are used to convert to and from properties in the table.

You can adjust conventions by configuring a NamingStrategy on CassandraMappingContext. Naming strategy objects implement the convention by which a table, column or user-defined type is derived from an entity class and from an actual property.

The following example shows how to configure a NamingStrategy:

Example 32. Configuring NamingStrategy on CassandraMappingContext
    CassandraMappingContext context = new CassandraMappingContext();

    // default naming strategy
    context.setNamingStrategy(NamingStrategy.INSTANCE);

    // snake_case converted to upper case (SNAKE_CASE)
    context.setNamingStrategy(NamingStrategy.SNAKE_CASE.transform(String::toUpperCase));

12.2.1. Mapping Configuration

Unless explicitly configured, an instance of MappingCassandraConverter is created by default when creating a CassandraTemplate. You can create your own instance of the MappingCassandraConverter to tell it where to scan the classpath at startup for your domain classes to extract metadata and construct indexes.

Also, by creating your own instance, you can register Spring Converter instances to use for mapping specific classes to and from the database. The following example configuration class sets up Cassandra mapping support:

Example 33. @Configuration class to configure Cassandra mapping support
@Configuration
public class SchemaConfiguration extends AbstractCassandraConfiguration {

  @Override
  protected String getKeyspaceName() {
    return "bigbank";
  }

  // the following are optional

  @Override
  public CassandraCustomConversions customConversions() {

    List<Converter<?, ?>> converters = new ArrayList<>();

    converters.add(new PersonReadConverter());
    converters.add(new PersonWriteConverter());

    return new CassandraCustomConversions(converters);
  }

  @Override
  public SchemaAction getSchemaAction() {
    return SchemaAction.RECREATE;
  }

  // other methods omitted...
}

AbstractCassandraConfiguration requires you to implement methods that define a keyspace. AbstractCassandraConfiguration also has a method named getEntityBasePackages(…). You can override it to tell the converter where to scan for classes annotated with the @Table annotation.

You can add additional converters to the MappingCassandraConverter by overriding the customConversions method.

AbstractCassandraConfiguration creates a CassandraTemplate instance and registers it with the container under the name of cassandraTemplate.

12.3. Metadata-based Mapping

To take full advantage of the object mapping functionality inside the Spring Data for Apache Cassandra support, you should annotate your mapped domain objects with the @Table annotation. Doing so lets the classpath scanner find and pre-process your domain objects to extract the necessary metadata. Only annotated entities are used to perform schema actions. In the worst case, a SchemaAction.RECREATE_DROP_UNUSED operation drops your tables and you lose your data. The following example shows a simple domain object:

Example 34. Example domain object
package com.mycompany.domain;

@Table
public class Person {

  @Id
  private String id;

  @CassandraType(type = Name.VARINT)
  private Integer ssn;

  private String firstName;

  private String lastName;
}
The @Id annotation tells the mapper which property you want to use for the Cassandra primary key. Composite primary keys can require a slightly different data model.

12.3.1. Working with Primary Keys

Cassandra requires at least one partition key field for a CQL table. A table can additionally declare one or more clustering key fields. When your CQL table has a composite primary key, you must create a @PrimaryKeyClass to define the structure of the composite primary key. In this context, “composite primary key” means one or more partition columns optionally combined with one or more clustering columns.

Primary keys can make use of any singular simple Cassandra type or mapped user-defined Type. Collection-typed primary keys are not supported.

Simple Primary Keys

A simple primary key consists of one partition key field within an entity class. Since it is one field only, we safely can assume it is a partition key. The following listing shows a CQL table defined in Cassandra with a primary key of user_id:

Example 35. CQL Table defined in Cassandra
CREATE TABLE user (
  user_id text,
  firstname text,
  lastname text,
  PRIMARY KEY (user_id))
;

The following example shows a Java class annotated such that it corresponds to the Cassandra defined in the previous listing:

Example 36. Annotated Entity
@Table(value = "login_event")
public class LoginEvent {

  @PrimaryKey("user_id")
  private String userId;

  private String firstname;
  private String lastname;

  // getters and setters omitted

}
Composite Keys

Composite primary keys (or compound keys) consist of more than one primary key field. That said, a composite primary key can consist of multiple partition keys, a partition key and a clustering key, or a multitude of primary key fields.

Composite keys can be represented in two ways with Spring Data for Apache Cassandra:

  • Embedded in an entity.

  • By using @PrimaryKeyClass.

The simplest form of a composite key is a key with one partition key and one clustering key.

The following example shows a CQL statement to represent the table and its composite key:

Example 37. CQL Table with a Composite Primary Key
CREATE TABLE login_event(
  person_id text,
  event_code int,
  event_time timestamp,
  ip_address text,
  PRIMARY KEY (person_id, event_code, event_time))
  WITH CLUSTERING ORDER BY (event_time DESC)
;
Flat Composite Primary Keys

Flat composite primary keys are embedded inside the entity as flat fields. Primary key fields are annotated with @PrimaryKeyColumn. Selection requires either a query to contain predicates for the individual fields or the use of MapId. The following example shows a class with a flat composite primary key:

Example 38. Using a flat composite primary key
@Table(value = "login_event")
class LoginEvent {

  @PrimaryKeyColumn(name = "person_id", ordinal = 0, type = PrimaryKeyType.PARTITIONED)
  private String personId;

  @PrimaryKeyColumn(name = "event_code", ordinal = 1, type = PrimaryKeyType.PARTITIONED)
  private int eventCode;

  @PrimaryKeyColumn(name = "event_time", ordinal = 2, type = PrimaryKeyType.CLUSTERED, ordering = Ordering.DESCENDING)
  private LocalDateTime eventTime;

  @Column("ip_address")
  private String ipAddress;

  // getters and setters omitted
}
Primary Key Class

A primary key class is a composite primary key class that is mapped to multiple fields or properties of the entity. It is annotated with @PrimaryKeyClass and should define equals and hashCode methods. The semantics of value equality for these methods should be consistent with the database equality for the database types to which the key is mapped. Primary key classes can be used with repositories (as the Id type) and to represent an entity’s identity in a single complex object. The following example shows a composite primary key class:

Example 39. Composite primary key class
@PrimaryKeyClass
class LoginEventKey implements Serializable {

  @PrimaryKeyColumn(name = "person_id", ordinal = 0, type = PrimaryKeyType.PARTITIONED)
  private String personId;

  @PrimaryKeyColumn(name = "event_code", ordinal = 1, type = PrimaryKeyType.PARTITIONED)
  private int eventCode;

  @PrimaryKeyColumn(name = "event_time", ordinal = 2, type = PrimaryKeyType.CLUSTERED, ordering = Ordering.DESCENDING)
  private LocalDateTime eventTime;

  // other methods omitted
}

The following example shows how to use a composite primary key:

Example 40. Using a composite primary key
@Table(value = "login_event")
public class LoginEvent {

  @PrimaryKey
  private LoginEventKey key;

  @Column("ip_address")
  private String ipAddress;

  // getters and setters omitted
}

12.3.2. Embedded Entity Support

Embedded entities are used to design value objects in your Java domain model whose properties are flattened out into the table. In the following example you see, that User.name is annotated with @Embedded. The consequence of this is that all properties of UserName are folded into the user table which consists of 3 columns (user_id, firstname, lastname).

Embedded entities may only contain simple property types. It is not possible to nest an embedded entity into another embedded one.

However, if the firstname and lastname column values are actually null within the result set, the entire property name will be set to null according to the onEmpty of @Embedded, which nulls objects when all nested properties are null.
Opposite to this behavior USE_EMPTY tries to create a new instance using either a default constructor or one that accepts nullable parameter values from the result set.

Example 41. Sample Code of embedding objects
public class User {

  @PrimaryKey("user_id")
    private String userId;

    @Embedded(onEmpty = USE_NULL) (1)
    UserName name;
}

public class UserName {
    private String firstname;
    private String lastname;
}
1 Property is null if firstname and lastname are null. Use onEmpty=USE_EMPTY to instantiate UserName with a potential null value for its properties.

You can embed a value object multiple times in an entity by using the optional prefix element of the @Embedded annotation. This element represents a prefix and is prepended to each column name in the embedded object. Note that properties will overwrite each other if multiple properties render to the same column name.

Make use of the shortcuts @Embedded.Nullable and @Embedded.Empty for @Embedded(onEmpty = USE_NULL) and @Embedded(onEmpty = USE_EMPTY) to reduce verbosity and simultaneously set JSR-305 @javax.annotation.Nonnull accordingly.

public class MyEntity {

    @Id
    Integer id;

    @Embedded.Nullable (1)
    EmbeddedEntity embeddedEntity;
}
1 Shortcut for @Embedded(onEmpty = USE_NULL).

12.3.3. Mapping Annotation Overview

The MappingCassandraConverter can use metadata to drive the mapping of objects to rows in a Cassandra table. An overview of the annotations follows:

  • @Id: Applied at the field or property level to mark the property used for identity purposes.

  • @Table: Applied at the class level to indicate that this class is a candidate for mapping to the database. You can specify the name of the table where the object is stored.

  • @PrimaryKey: Similar to @Id but lets you specify the column name.

  • @PrimaryKeyColumn: Cassandra-specific annotation for primary key columns that lets you specify primary key column attributes, such as for clustered or partitioned. Can be used on single and multiple attributes to indicate either a single or a composite (compound) primary key. If used on a property within the entity, make sure to apply the @Id annotation as well.

  • @PrimaryKeyClass: Applied at the class level to indicate that this class is a compound primary key class. Must be referenced with @PrimaryKey in the entity class.

  • @Transient: By default, all private fields are mapped to the row. This annotation excludes the field where it is applied from being stored in the database. Transient properties cannot be used within a persistence constructor as the converter cannot materialize a value for the constructor argument.

  • @PersistenceConstructor: Marks a given constructor — even a package protected one — to use when instantiating the object from the database. Constructor arguments are mapped by name to the key values in the retrieved row.

  • @Value: This annotation is part of the Spring Framework . Within the mapping framework it can be applied to constructor arguments. This lets you use a Spring Expression Language statement to transform a key’s value retrieved in the database before it is used to construct a domain object. In order to reference a property of a given Row/UdtValue/TupleValue one has to use expressions like: @Value("#root.getString(0)") where root refers to the root of the given document.

  • @ReadOnlyProperty: Applies at the field level to mark a property as read-only. Entity-bound insert and update statements do not include this property.

  • @Column: Applied at the field level. Describes the column name as it is represented in the Cassandra table, thus letting the name differ from the field name of the class. Can be used on constructor arguments to customize the column name during constructor creation.

  • @Embedded: Applied at the field level. Enables embedded object usage for types mapped to a table or a user-defined type. Properties of the embedded object are flattened into the structure of its parent.

  • @Indexed: Applied at the field level. Describes the index to be created at session initialization.

  • @SASI: Applied at the field level. Allows SASI index creation during session initialization.

  • @CassandraType: Applied at the field level to specify a Cassandra data type. Types are derived from the property declaration by default.

  • @Frozen: Applied at the field level to class-types and parametrized types. Declares a frozen UDT column or frozen collection like List<@Frozen UserDefinedPersonType>.

  • @UserDefinedType: Applied at the type level to specify a Cassandra User-defined Data Type (UDT). Types are derived from the declaration by default.

  • @Tuple: Applied at the type level to use a type as a mapped tuple.

  • @Element: Applied at the field level to specify element or field ordinals within a mapped tuple. Types are derived from the property declaration by default. Can be used on constructor arguments to customize tuple element ordinals during constructor creation.

  • @Version: Applied at field level is used for optimistic locking and checked for modification on save operations. The initial value is zero which is bumped automatically on every update.

The mapping metadata infrastructure is defined in the separate, spring-data-commons project that is both technology- and data store-agnostic.

The following example shows a more complex mapping:

Example 42. Mapped Person class
@Table("my_person")
public class Person {

  @PrimaryKeyClass
  public static class Key implements Serializable {

    @PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED)
    private String type;

    @PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.PARTITIONED)
    private String value;

    @PrimaryKeyColumn(name = "correlated_type", ordinal = 2, type = PrimaryKeyType.CLUSTERED)
    private String correlatedType;

    // other getters/setters omitted
  }

  @PrimaryKey
  private Person.Key key;

  @CassandraType(type = CassandraType.Name.VARINT)
  private Integer ssn;

  @Column("f_name")
  private String firstName;

  @Column
  @Indexed
  private String lastName;

  private Address address;

  @CassandraType(type = CassandraType.Name.UDT, userTypeName = "myusertype")
  private UdtValue usertype;

  private Coordinates coordinates;

  @Transient
  private Integer accountTotal;

  @CassandraType(type = CassandraType.Name.SET, typeArguments = CassandraType.Name.BIGINT)
  private Set<Long> timestamps;

  private Map<@Indexed String, InetAddress> sessions;

  public Person(Integer ssn) {
    this.ssn = ssn;
  }

  public Person.Key getKey() {
    return key;
  }

  // no setter for Id.  (getter is only exposed for some unit testing)

  public Integer getSsn() {
    return ssn;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  // other getters/setters omitted
}

The following example shows how to map a UDT Address:

Example 43. Mapped User-Defined Type Address
@UserDefinedType("address")
public class Address {

  @CassandraType(type = CassandraType.Name.VARCHAR)
  private String street;

  private String city;

  private Set<String> zipcodes;

  @CassandraType(type = CassandraType.Name.SET, typeArguments = CassandraType.Name.BIGINT)
  private List<Long> timestamps;

  // other getters/setters omitted
}
Working with User-Defined Types requires a UserTypeResolver that is configured with the mapping context. See the configuration chapter for how to configure a UserTypeResolver.

The following example shows how map a tuple:

Example 44. Mapped Tuple
@Tuple
class Coordinates {

  @Element(0)
  @CassandraType(type = CassandraType.Name.VARCHAR)
  private String description;

  @Element(1)
  private long longitude;

  @Element(2)
  private long latitude;

  // other getters/setters omitted
}
Index Creation

You can annotate particular entity properties with @Indexed or @SASI if you wish to create secondary indexes on application startup. Index creation creates simple secondary indexes for scalar types, user-defined types, and collection types.

You can configure a SASI Index to apply an analyzer, such as StandardAnalyzer or NonTokenizingAnalyzer (by using @StandardAnalyzed and @NonTokenizingAnalyzed, respectively).

Map types distinguish between ENTRY, KEYS, and VALUES indexes. Index creation derives the index type from the annotated element. The following example shows a number of ways to create an index:

Example 45. Variants of map indexing
@Table
class PersonWithIndexes {

  @Id
  private String key;

  @SASI
  @StandardAnalyzed
  private String names;

  @Indexed("indexed_map")
  private Map<String, String> entries;

  private Map<@Indexed String, String> keys;

  private Map<String, @Indexed String> values;

  // …
}

The @Indexed annotation can be applied to single properties of embedded entities or along side with the @Embedded annotation, in which case all properties of the embedded are indexed.

Index creation on session initialization may have a severe performance impact on application startup.

12.4. Overriding Default Mapping with Custom Converters

To have more fine-grained control over the mapping process, you can register Spring Converters with CassandraConverter implementations, such as MappingCassandraConverter.

MappingCassandraConverter first checks to see whether any Spring Converters can handle a specific class before attempting to map the object itself. To "'hijack'" the normal mapping strategies of the MappingCassandraConverter (perhaps for increased performance or other custom mapping needs), you need to create an implementation of the Spring Converter interface and register it with the MappingCassandraConverter.

12.4.1. Saving by Using a Registered Spring Converter

You can combine converting and saving in a single process, basically using the converter to do the saving.

The following example uses a Converter to convert a Person object to a java.lang.String with Jackson 2:

class PersonWriteConverter implements Converter<Person, String> {

  public String convert(Person source) {

    try {
      return new ObjectMapper().writeValueAsString(source);
    } catch (IOException e) {
      throw new IllegalStateException(e);
    }
  }
}

12.4.2. Reading by Using a Spring Converter

Similar to how you can combine saving and converting, you can also combine reading and converting.

The following example uses a Converter that converts a java.lang.String into a Person object with Jackson 2:

class PersonReadConverter implements Converter<String, Person> {

  public Person convert(String source) {

    if (StringUtils.hasText(source)) {
      try {
        return new ObjectMapper().readValue(source, Person.class);
      } catch (IOException e) {
        throw new IllegalStateException(e);
      }
    }

    return null;
  }
}

12.4.3. Registering Spring Converters with CassandraConverter

Spring Data for Apache Cassandra Java configuration provides a convenient way to register Spring Converter instances: MappingCassandraConverter. The following configuration snippet shows how to manually register converters as well as configure CustomConversions:

@Configuration
public class ConverterConfiguration extends AbstractCassandraConfiguration {

  @Override
  public CassandraCustomConversions customConversions() {

    List<Converter<?, ?>> converters = new ArrayList<>();

    converters.add(new PersonReadConverter());
    converters.add(new PersonWriteConverter());

    return new CassandraCustomConversions(converters);
  }

  // other methods omitted...

}

Unresolved directive in reference/converters.adoc - include::../../../../../../spring-data-commons/src/main/asciidoc/custom-conversions.adoc[leveloffset=+3]

Unresolved directive in reference/mapping.adoc - include::../../../../../../spring-data-commons/src/main/asciidoc/is-new-state-detection.adoc[leveloffset=+1]

Cassandra provides no means to generate identifiers upon inserting data. As consequence, entities must be associated with identifier values. Spring Data defaults to identifier inspection to determine whether an entity is new. If you want to use auditing make sure to either use Optimistic Locking or implement Persistable for proper entity state detection.

12.5. Lifecycle Events

The Cassandra mapping framework has several built-in org.springframework.context.ApplicationEvent events that your application can respond to by registering special beans in the ApplicationContext. Being based on Spring’s application context event infrastructure lets other products, such as Spring Integration, easily receive these events as they are a well known eventing mechanism in Spring-based applications.

To intercept an object before it goes into the database, you can register a subclass of org.springframework.data.cassandra.core.mapping.event.AbstractCassandraEventListener that overrides the onBeforeSave(…) method. When the event is dispatched, your listener is called and passed the domain object (which is a Java entity). The following example uses the onBeforeSave method:

class BeforeSaveListener extends AbstractCassandraEventListener<Person> {
  @Override
  public void onBeforeSave(BeforeSaveEvent<Person> event) {
    // … change values, delete them, whatever …
  }
}

Declaring these beans in your Spring ApplicationContext will cause them to be invoked whenever the event is dispatched.

The AbstractCassandraEventListener has the following callback methods:

  • onBeforeSave: Called in CassandraTemplate.insert(…) and .update(…) operations before inserting or updating a row in the database.

  • onAfterSave: Called in CassandraTemplate…insert(…) and .update(…) operations after inserting or updating a row in the database.

  • onBeforeDelete: Called in CassandraTemplate.delete(…) operations before deleting row from the database.

  • onAfterDelete: Called in CassandraTemplate.delete(…) operations after deleting row from the database.

  • onAfterLoad: Called in the CassandraTemplate.select(…), .slice(…), and .stream(…) methods after each row is retrieved from the database.

  • onAfterConvert: Called in the CassandraTemplate.select(…), .slice(…), and .stream(…) methods after converting a row retrieved from the database to a POJO.

Lifecycle events are emitted only for root-level types. Complex types used as properties within an aggregate root are not subject to event publication.

Unresolved directive in reference/mapping.adoc - include::../../../../../../spring-data-commons/src/main/asciidoc/entity-callbacks.adoc[leveloffset=+1] :leveloffset: +2

13. Store specific EntityCallbacks

Spring Data for Apache Cassandra uses the EntityCallback API for its auditing support and reacts on the following callbacks.

Table 4. Supported Entity Callbacks
Callback Method Description Order

Reactive/BeforeConvertCallback

onBeforeConvert(T entity, CqlIdentifier tableName)

Invoked before a domain object is converted to com.datastax.driver.core.Statement.

Ordered.LOWEST_PRECEDENCE

Reactive/AuditingEntityCallback

onBeforeConvert(Object entity, CqlIdentifier tableName)

Marks an auditable entity created or modified

100

Reactive/BeforeSaveCallback

onBeforeSave(T entity, CqlIdentifier tableName, Statement statement)

Invoked before a domain object is saved.
Can modify the target, to be persisted, com.datastax.driver.core.Statement containing all mapped entity information.

Ordered.LOWEST_PRECEDENCE

Unresolved directive in reference/kotlin.adoc - include::../../../../../../spring-data-commons/src/main/asciidoc/kotlin.adoc[]

Unresolved directive in reference/kotlin.adoc - include::../../../../../../spring-data-commons/src/main/asciidoc/kotlin-extensions.adoc[leveloffset=+1]

To retrieve a list of SWCharacter objects in Java, you would normally write the following:

Flux<SWCharacter> characters = template.query(SWCharacter.class).inTable("star-wars").all()

With Kotlin and the Spring Data extensions, you can instead write the following:

val characters = template.query<SWCharacter>().inTable("star-wars").all()
// or (both are equivalent)
val characters : Flux<SWCharacter> = template.query().inTable("star-wars").all()

As in Java, characters in Kotlin is strongly typed, but Kotlin’s clever type inference allows for shorter syntax.

Spring Data for Apache Cassandra provides the following extensions:

  • Reified generics support for CassandraOperations (including async and reactive variants), CqlOperations (including async and reactive variants)FluentCassandraOperations, ReactiveFluentCassandraOperations, Criteria, and Query.

  • [kotlin.coroutines] extensions for ReactiveFluentCassandraOperations.

Unresolved directive in reference/kotlin.adoc - include::../../../../../../spring-data-commons/src/main/asciidoc/kotlin-coroutines.adoc[leveloffset=+1]

Appendix

Unresolved directive in index.adoc - include::../../../../../spring-data-commons/src/main/asciidoc/repository-namespace-reference.adoc[leveloffset=+1] Unresolved directive in index.adoc - include::../../../../../spring-data-commons/src/main/asciidoc/repository-populator-namespace-reference.adoc[leveloffset=+1] Unresolved directive in index.adoc - include::../../../../../spring-data-commons/src/main/asciidoc/repository-query-keywords-reference.adoc[leveloffset=+1] Unresolved directive in index.adoc - include::../../../../../spring-data-commons/src/main/asciidoc/repository-query-return-types-reference.adoc[leveloffset=+1] :leveloffset: +1

Appendix E: Migration Guides

Migration Guide from Spring Data Cassandra 1.x to 2.x

Spring Data for Apache Cassandra 2.0 introduces a set of breaking changes when upgrading from earlier versions:

  • Merged the spring-cql and spring-data-cassandra modules into a single module.

  • Separated asynchronous and synchronous operations in CqlOperations and CassandraOperations into dedicated interfaces and templates.

  • Revised the CqlTemplate API to align with JdbcTemplate.

  • Removed the CassandraOperations.selectBySimpleIds method.

  • Used better names for CassandraRepository.

  • Removed SD Cassandra ConsistencyLevel and RetryPolicy types in favor of DataStax ConsistencyLevel and RetryPolicy types.

  • Refactored CQL specifications to value objects and configurators.

  • Refactored QueryOptions to be immutable objects.

  • Refactored CassandraPersistentProperty to single-column.

Deprecations

  • Deprecated QueryOptionsBuilder.readTimeout(long, TimeUnit) in favor of QueryOptionsBuilder.readTimeout(Duration).

  • Deprecated CustomConversions in favor of CassandraCustomConversions.

  • Deprecated BasicCassandraMappingContext in favor of CassandraMappingContext.

  • Deprecated o.s.d.c.core.cql.CachedPreparedStatementCreator in favor of o.s.d.c.core.cql.support.CachedPreparedStatementCreator.

  • Deprecated CqlTemplate.getSession() in favor of getSessionFactory().

  • Deprecated CqlIdentifier.cqlId(…) and KeyspaceIdentifier.ksId(…) in favor of the .of(…) methods.

  • Deprecated constructors of QueryOptions in favor of their builders.

  • Deprecated TypedIdCassandraRepository in favor of CassandraRepository

Merged Spring CQL and Spring Data Cassandra Modules

Spring CQL and Spring Data Cassandra are now merged into a single module. The standalone spring-cql module is no longer available. You can find all types merged into spring-data-cassandra. The following listing shows how to include spring-data-cassandra in your maven dependencies:

<dependencies>

  <dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-cassandra</artifactId>
    <version>3.3.0-SNAPSHOT</version>
  </dependency>

</dependencies>

With the merge, we merged all CQL packages into Spring Data Cassandra:

  • Moved o.s.d.cql into o.s.d.cassandra.core.cql.

  • Merged o.s.d.cql with o.s.d.cassandra.config and flattened the XML and Java subpackages.

  • Moved CassandraExceptionTranslator and CqlExceptionTranslator to o.s.d.c.core.cql.

  • Moved Cassandra exceptions o.s.d.c.support.exception to o.s.d.cassandra.

  • Moved o.s.d.c.convert to o.s.d.c.core.convert (affects converters).

  • Moved o.s.d.c.mapping to o.s.d.c.core.mapping (affects mapping annotations).

  • Moved MapId from o.s.d.c.repository to o.s.d.c.core.mapping.

Revised CqlTemplate/CassandraTemplate

We split CqlTemplate and CassandraTemplate in three ways:

  • CassandraTemplate is no longer a CqlTemplate but uses an instance that allows reuse and fine-grained control over fetch size, consistency levels, and retry policies. You can obtain the CqlOperations through CassandraTemplate.getCqlOperations(). Because of the change, dependency injection of CqlTemplate requires additional bean setup.

  • CqlTemplate now reflects basic CQL operations instead of mixing high-level and low-level API calls (such as count(…) versus execute(…)) and the reduced method set is aligned with Spring Frameworks’s JdbcTemplate with its convenient callback interfaces.

  • Asynchronous methods are re-implemented on AsyncCqlTemplate and AsyncCassandraTemplate by using ListenableFuture. We removed Cancellable and the various async callback listeners. ListenableFuture is a flexible approach and allows transition into a CompletableFuture.

Removed CassandraOperations.selectBySimpleIds()

The method was removed because it did not support complex IDs. The newly introduced query DSL allows mapped and complex id’s for single column Id’s, as the following example shows:

cassandraTemplate.select(Query.query(Criteria.where("id").in(…)), Person.class)

Better names for CassandraRepository

We renamed CassandraRepository and TypedIdCassandraRepository to align Spring Data Cassandra naming with other Spring Data modules:

  • Renamed CassandraRepository to MapIdCassandraRepository

  • Renamed TypedIdCassandraRepository to CassandraRepository

  • Introduced TypedIdCassandraRepository, extending CassandraRepository as a deprecated type to ease migration

Removed SD Cassandra ConsistencyLevel and RetryPolicy types in favor of DataStax ConsistencyLevel and RetryPolicy types

Spring Data Cassandra ConsistencyLevel and RetryPolicy have been removed. Please use the types provided by the DataStax driver.

The Spring Data Cassandra types restricted usage of available features provided in and allowed by the Cassandra native driver. As a result, the Spring Data Cassandra’s types required an update each time newer functionality was introduced by the driver.

Refactored CQL Specifications to Value Objects and Configurators

As much as possible, CQL specification types are now value types (such as FieldSpecification, AlterColumnSpecification), and objects are constructed by static factory methods. This allows immutability for simple value objects. Configurator objects (such as AlterTableSpecification) that operate on mandatory properties (such as a table name or keyspace name) are initially constructed through a a static factory method and allow further configuration until the desired state is created.

Refactored QueryOptions to be Immutable Objects

QueryOptions and WriteOptions are now immutable and can be created through builders. Methods accepting QueryOptions enforce non-null objects, which are available from static empty() factory methods. The following example shows how to use QueryOptions.builder():

QueryOptions queryOptions = QueryOptions.builder()
    .consistencyLevel(ConsistencyLevel.ANY)
    .retryPolicy(FallthroughRetryPolicy.INSTANCE)
    .readTimeout(Duration.ofSeconds(10))
    .fetchSize(10)
    .tracing(true)
    .build();

Refactored CassandraPersistentProperty to Single-column

This change affects You only if you operate directly on the mapping model.

CassandraPersistentProperty allowed previously multiple column names to be bound for composite primary key use. Columns of a CassandraPersistentProperty are now reduced to a single column. Resolved composite primary keys map to a class through MappingContext.getRequiredPersistentEntity(…).

Migration Guide from Spring Data Cassandra 2.x to 3.x

Spring Data for Apache Cassandra 3.0 introduces a set of breaking changes when upgrading from earlier versions.

Review dependencies

Upgrading to Spring Data Cassandra requires an upgrade to the DataStax Driver version 4. Upgrading to the new driver comes with transitive dependency changes, most notably, Google Guava is bundled and shaded by the driver. Check out the DataStax Java Driver for Apache Cassandra 4 Upgrade Guide for details on the Driver-related changes.

Adapt Configuration

DataStax Java Driver 4 merges Cluster and Session objects into a single CqlSession object, therefore, all Cluster-related API was removed. The configuration was revised in large parts by removing most configuration items that were moved into DriverConfigLoader that is mostly file-based. This means that SocketOptions, AddressTranslator and many more options are configured now through other means.

To reflect the change in configuration builders, ClusterBuilderConfigurer was renamed to SessionBuilderConfigurer accepting now CqlSessionBuilder instead of the Cluster.Builder. Make sure to also provide the local data center in your configuration as it is required to properly configure load balancing.

Connectivity

The configuration elements for Cluster (cassandra:cluster) and Session (cassandra:session) were merged into a single CqlSession (cassandra:session) element that configures both, the keyspace and endpoints.

With the upgrade, schema support was moved to a new namespace element: cassandra:session-factory that provides a SessionFactory bean.

Example 46. Cluster, Session and Schema Configuration in version 2:
<cassandra:cluster contact-points="localhost" port="9042">
  <cassandra:keyspace action="CREATE_DROP" name="mykeyspace" />
</cassandra:cluster>

<cassandra:session keyspace-name="mykeyspace" schema-action="CREATE">
  <cassandra:startup-cql>CREATE TABLE …</cassandra:startup-cql>
</cassandra:session>
Example 47. Session and Schema Configuration in version 3:
<cassandra:session contact-points="localhost" port="9042" keyspace="mykeyspace" local-datacenter="datacenter1">
  <cassandra:keyspace action="CREATE_DROP" name="mykeyspace" />
</cassandra:session>

<cassandra:session-factory schema-action="CREATE">
  <cassandra:script location="classpath:/schema.cql"/>
</cassandra:session-factory>
Spring Data Cassandra 3.0 no longer registers default Mapping Context, Context and Template API beans when using XML namespace configuration. The defaulting should be applied on application or Spring Boot level.

Template API

Spring Data for Apache Cassandra encapsulates most of the changes that come with the driver upgrade as the Template API and repository support if your application mainly interacts with mapped entities or primitive Java types.

We generally recommend to create CqlTemplate and CassandraTemplate objects by using SessionFactory as the factory usage allows synchronization for schema creation and introduces a level of flexibility when working with multiple databases.

Example 48. Template API configuration in version 2:
<cql:template session-ref="…" />

<cassandra:template session-ref="…" cassandra-converter-ref="…"/>
Example 49. Template API configuration in version 3:
<cassandra:session-factory />

<cassandra:cql-template session-factory-ref="…" />

<cassandra:template session-factory-ref="…" cassandra-converter-ref="…"/>

You will have to adapt your code in all places, where you use DataStax driver API directly. Typical cases include:

  • Implementations of ResultSetExtractor

  • Implementations of RowCallbackHandler

  • Implementations of RowMapper

  • Implementations of PreparedStatementCreator including async and reactive variants

  • Calls to CqlTemplate.queryForResultSet(…)

  • Calling methods that accept Statement

Changes in AsyncCqlTemplate

DataStax driver 4 has changed the result type of queries that are run asynchronously. To reflect these changes, you need to adapt your code that provides:

  • Implementations of AsyncSessionCallback

  • Implementations of AsyncPreparedStatementCreator

Result set extraction requires a new interface for DataStax' AsyncResultSet. AsyncCqlTemplate now uses AsyncResultSetExtractor in places where it used previously ResultSetExtractor. Note that AsyncResultSetExtractor.extractData(…) returns a Future instead of a scalar object so a migration of code comes with the possibility to use fully non-blocking code in the extractor.

Data model migrations

Your data model may require updates if you use the following features:

  • @CassandraType

  • forceQuote in @Table, @Column, @PrimaryKeyColumn, @PrimaryKey and @UserDefinedType

  • Properties using java.lang.Date

  • Properties using UDTValue or TupleValue

@CassandraType

DataStax driver 4 no longer ships with a Name enumeration to describe the Cassandra type. We decided to re-introduce the enumeration with CassandraType.Name. Make sure to update your imports to use the newly introduced replacement type.

Force Quote

This flag is now deprecated, and we recommend not to use it any longer. Spring Data for Apache Cassandra internally uses the driver’s CqlIdentifier that ensures quoting where it’s required.

Property Types

DataStax driver 4 no longer uses java.lang.Date. Please upgrade your data model to use java.time.LocalDateTime. Please also migrate raw UDT and tuple types to the new driver types UdtValue respective TupleValue.

Other changes

  • Driver’s ConsistencyLevel constant class was removed and reintroduced as DefaultConsistencyLevel. @Consistency was adapted to DefaultConsistencyLevel.

  • RetryPolicy on QueryOptions and …CqlTemplate types was removed without replacement.

  • Drivers’s PagingState type was removed. Paging state now uses ByteBuffer.

  • SimpleUserTypeResolver accepts CqlSession instead of Cluster.

  • SimpleTupleTypeFactory was migrated to enum. SimpleTupleTypeFactory.INSTANCE no longer requires a Cluster/CqlSession context.

  • Introduction of StatementBuilder to functionally build statements as the QueryBuilder API uses immutable statement types.

  • Session bean renamed from session to cassandraSession and SessionFactory bean renamed from sessionFactory to cassandraSessionFactory.

  • ReactiveSession bean renamed from reactiveSession to reactiveCassandraSession and ReactiveSessionFactory bean renamed from reactiveSessionFactory to reactiveCassandraSessionFactory.

  • ReactiveSessionFactory.getSession() now returns a Mono<ReactiveSession>. Previously it returned just ReactiveSession.

  • Data type resolution was moved into ColumnTypeResolver so all DataType-related methods were moved from CassandraPersistentEntity/CassandraPersistentProperty into ColumnTypeResolver (affected methods are MappingContext.getDataType(…), CassandraPersistentProperty.getDataType(), CassandraPersistentEntity.getUserType(), and CassandraPersistentEntity.getTupleType()).

  • Schema creation was moved from MappingContext to SchemaFactory (affected methods are CassandraMappingContext.getCreateTableSpecificationFor(…), CassandraMappingContext.getCreateIndexSpecificationsFor(…), and CassandraMappingContext.getCreateUserTypeSpecificationFor(…)).

Deprecations

  • CassandraCqlSessionFactoryBean, use CqlSessionFactoryBean instead.

  • KeyspaceIdentifier and CqlIdentifier, use com.datastax.oss.driver.api.core.CqlIdentifier instead.

  • CassandraSessionFactoryBean, use CqlSessionFactoryBean instead.

  • AbstractCqlTemplateConfiguration, use AbstractSessionConfiguration instead.

  • AbstractSessionConfiguration.getClusterName(), use AbstractSessionConfiguration.getSessionName() instead.

  • CodecRegistryTupleTypeFactory, use SimpleTupleTypeFactory instead.

  • Spring Data’s CqlIdentifier, use the driver CqlIdentifier instead.

  • forceQuote attributes as quoting is no longer required. CqlIdentifier properly escapes reserved keywords and takes care of case-sensitivity.

  • fetchSize on QueryOptions and …CqlTemplate types was deprecated, use pageSize instead

  • CassandraMappingContext.setUserTypeResolver(…), CassandraMappingContext.setCodecRegistry(…), and CassandraMappingContext.setCustomConversions(…): Configure these properties on CassandraConverter.

  • TupleTypeFactory and CassandraMappingContext.setTupleTypeFactory(…): TupleTypeFactory is no longer used as the Cassandra driver ships with a DataTypes.tupleOf(…) factory method.

  • Schema creation via CqlSessionFactoryBean (cassandra:session) is deprecated. Keyspace creation via CqlSessionFactoryBean (cassandra:session) is not affected.

Removals

Configuration API

  • PoolingOptionsFactoryBean

  • SocketOptionsFactoryBean

  • CassandraClusterFactoryBean

  • CassandraClusterParser

  • CassandraCqlClusterFactoryBean

  • CassandraCqlClusterParser

  • CassandraCqlSessionParser

  • AbstractClusterConfiguration

  • ClusterBuilderConfigurer (use SessionBuilderConfigurer instead

Utilities

  • GuavaListenableFutureAdapter

  • QueryOptions and WriteOptions constructor taking ConsistencyLevel and RetryPolicy arguments. Use the builder in conjunction of execution profiles as replacement.

  • CassandraAccessor.setRetryPolicy(…) and ReactiveCqlTemplate.setRetryPolicy(…) methods. Use execution profiles as replacement.

Namespace support

Additions

Configuration API

  • CqlSessionFactoryBean

  • InitializeKeyspaceBeanDefinitionParser

  • SessionFactoryFactoryBean including schema creation via KeyspacePopulator

  • KeyspacePopulator and SessionFactoryInitializer to initialize a keyspace

Namespace support

  • cassandra:cluster (endpoint properties merged to cassandra:session)

  • cassandra:initialize-keyspace namespace support

  • cassandra:session-factory with cassandra:script support