© 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:
-
The DataStax site offers commercial support and many resources, including, but not limited to, documentation, DataStax Academy, a Tech Blog, and so on.
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).
4.2. Project Metadata
-
Version Control: https://github.com/spring-projects/spring-data-cassandra
-
Bugtracker: https://github.com/spring-projects/spring-data-cassandra/issues
-
Release repository: https://repo.spring.io/libs-release
-
Milestone repository: https://repo.spring.io/libs-milestone
-
Snapshot repository: https://repo.spring.io/libs-snapshot
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 registersReactiveAuditingEntityCallback
. -
Reactive SpEL support in
@Query
query methods. -
Configuration of the keyspace per
Statement
throughCqlTemplate
andQueryOptions
. -
Revised
CqlOperations
with newqueryForStream(…)
methods returning aStream
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
-
Upgrade to Cassandra Driver version 4. See the 2.x to 3.x migration guide for details.
-
Support for
NamingStrategy
. -
Support for frozen collections and UDT columns in schema creation.
-
Support for
@Embedded
properties.
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-boundINSERT
andUPDATE
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(…)
andexists(…)
methods acceptingQuery
. -
Fluent API for CRUD operations.
-
Cassandra Mapped Tuple support via
@Tuple
. -
Support for Cassandra
time
columns viaLocalTime
. -
Support for
map
columns using User-defined/converted types. -
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.
-
Merged
spring-cql
andspring-data-cassandra
modules into a single module and re-packagedorg.springframework.cql
toorg.springframework.data.cassandra
. -
Revised
CqlTemplate
,AsyncCqlTemplate
,CassandraTemplate
andAsyncCassandraTemplate
implementations. -
Added routing capabilities via
SessionFactory
andAbstractRoutingSessionFactory
. -
Introduced
Update
andQuery
objects. -
Renamed CRUD Repository interface:
CassandraRepository
usingMapId
is now renamed toMapIdCassandraRepository
.TypedIdCassandraRepository
is renamed toCassandraRepository
. -
Pagination via
PagingState
andCassandraPageRequest
. -
Interface and DTO projections in Repository query methods.
-
Lightweight transaction support via
InsertOptions
andUpdateOptions
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 touuid
column type, was previouslytimeuuid
.
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
andQueryOptions
onCluster
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.
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
andReactiveCqlTemplate
are the classic Spring CQL approach and the most popular. This is the “lowest-level” approach. Note that components likeCassandraTemplate
useCqlTemplate
under-the-hood. -
CassandraTemplate
wraps aCqlTemplate
to provide query result-to-object mapping and the use ofSELECT
,INSERT
,UPDATE
, andDELETE
methods instead of writing CQL statements. This approach provides better documentation and ease of use. -
ReactiveCassandraTemplate
wraps aReactiveCqlTemplate
to provide query result-to-object mapping and the use ofSELECT
,INSERT
,UPDATE
, andDELETE
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 CassandraCqlSession
. -
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
:
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:
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:
@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
:
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:
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
<!-- 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:
<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:
@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
orSmartLifecycle
. When the application context starts, you can automatically start aSmartLifecycle
by setting itsautoStartup
flag, and you can manually start aLifecycle
by callingConfigurableApplicationContext.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 theSmartLifecycle
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 separateApplicationContext
instances (for example, the parent context contains theSessionFactory
, 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
: LikeSchemaAction.CREATE
but withIF 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:
<cassandra:mapping entity-base-packages="com.foo,com.bar"/>
The following example shows how to specify entity base packages in 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:
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 appliesInsertOptions
.
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 appliesUpdateOptions
.
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 newCassandraBatchOperations
to populate the batch.
CassandraBatchOperations
-
insert
: Takes a single object, an array (var-args), or anIterable
of objects to insert. -
update
: Takes a single object, an array (var-args), or anIterable
of objects to update. -
delete
: Takes a single object, an array (var-args), or anIterable
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:
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 byQuery
. -
T
delete(T entity)
: Deletes the given object. -
T
delete(T entity, QueryOptions queryOptions)
: Deletes the given object applyingQueryOptions
. -
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:
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 theIN
operator for a varargs argument. -
CriteriaDefinition
in(Collection<?> collection)
: Creates a criterion by using theIN
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 theLIKE
operator. -
CriteriaDefinition
contains(Object value)
: Creates a criterion by using theCONTAINS
operator. -
CriteriaDefinition
containsKey(Object key)
: Creates a criterion by using theCONTAINS 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 aQuery
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 associateSort
,PagingState
, andfetchSize
with the query (used for paging). -
Query
pagingState(ByteBuffer pagingState)
: Used to associate aByteBuffer
with the query (used for paging). -
Query
queryOptions(QueryOptions queryOptions)
: Used to associateQueryOptions
with the query. -
Query
sort(Sort sort)
: Used to provide a sort definition for the results. -
Query
withAllowFiltering()
: Used to renderALLOW 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 typeT
from the table. -
T
selectOne(Query query, Class<T> entityClass)
: Query for a single object of typeT
from the table. -
Slice<T>
slice(Query query, Class<T> entityClass)
: Starts or continues paging by querying for aSlice
of objects of typeT
from the table. -
Stream<T>
stream(Query query, Class<T> entityClass)
: Query for a stream of objects of typeT
from the table. -
List<T>
select(String cql, Class<T> entityClass)
: Ad-hoc query for a list of objects of typeT
from the table by providing a CQL statement. -
T
selectOne(String cql, Class<T> entityClass)
: Ad-hoc query for a single object of typeT
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 typeT
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 usingReactiveCassandraOperations
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 CassandraCqlSession
. -
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:
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 appliesInsertOptions
.
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 appliesUpdateOptions
.
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:
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:
@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:
Person
entitiespublic 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:
@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:
<?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
:
@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:
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:
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:
Keyword | Sample | Logical result |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
@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:
Person
entitiespublic 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:
@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:
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:
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 that use String queries and Query Derivation
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:
<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:
@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
.
@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:
Type | Cassandra types |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
user type |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
@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 namedsavingsaccount
. -
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
:
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:
@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:
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
:
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:
@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:
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:
@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:
@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:
@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 null
s 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.
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
|
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 givenRow
/UdtValue
/TupleValue
one has to use expressions like:@Value("#root.getString(0)")
whereroot
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 likeList<@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 iszero
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:
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
:
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:
@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:
@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 |
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 inCassandraTemplate.insert(…)
and.update(…)
operations before inserting or updating a row in the database. -
onAfterSave
: Called inCassandraTemplate…insert(…)
and.update(…)
operations after inserting or updating a row in the database. -
onBeforeDelete
: Called inCassandraTemplate.delete(…)
operations before deleting row from the database. -
onAfterDelete
: Called inCassandraTemplate.delete(…)
operations after deleting row from the database. -
onAfterLoad
: Called in theCassandraTemplate.select(…)
,.slice(…)
, and.stream(…)
methods after each row is retrieved from the database. -
onAfterConvert
: Called in theCassandraTemplate.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.
Callback | Method | Description | Order |
---|---|---|---|
Reactive/BeforeConvertCallback |
|
Invoked before a domain object is converted to |
|
Reactive/AuditingEntityCallback |
|
Marks an auditable entity created or modified |
100 |
Reactive/BeforeSaveCallback |
|
Invoked before a domain object is saved. |
|
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
, andQuery
. -
[kotlin.coroutines] extensions for
ReactiveFluentCassandraOperations
.
Unresolved directive in reference/kotlin.adoc - include::../../../../../../spring-data-commons/src/main/asciidoc/kotlin-coroutines.adoc[leveloffset=+1]
Appendix
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
andspring-data-cassandra
modules into a single module. -
Separated asynchronous and synchronous operations in
CqlOperations
andCassandraOperations
into dedicated interfaces and templates. -
Revised the
CqlTemplate
API to align withJdbcTemplate
. -
Removed the
CassandraOperations.selectBySimpleIds
method. -
Used better names for
CassandraRepository
. -
Removed SD Cassandra
ConsistencyLevel
andRetryPolicy
types in favor of DataStaxConsistencyLevel
andRetryPolicy
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 ofQueryOptionsBuilder.readTimeout(Duration)
. -
Deprecated
CustomConversions
in favor ofCassandraCustomConversions
. -
Deprecated
BasicCassandraMappingContext
in favor ofCassandraMappingContext
. -
Deprecated
o.s.d.c.core.cql.CachedPreparedStatementCreator
in favor ofo.s.d.c.core.cql.support.CachedPreparedStatementCreator
. -
Deprecated
CqlTemplate.getSession()
in favor ofgetSessionFactory()
. -
Deprecated
CqlIdentifier.cqlId(…)
andKeyspaceIdentifier.ksId(…)
in favor of the.of(…)
methods. -
Deprecated constructors of
QueryOptions
in favor of their builders. -
Deprecated
TypedIdCassandraRepository
in favor ofCassandraRepository
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
intoo.s.d.cassandra.core.cql
. -
Merged
o.s.d.cql
witho.s.d.cassandra.config
and flattened the XML and Java subpackages. -
Moved
CassandraExceptionTranslator
andCqlExceptionTranslator
too.s.d.c.core.cql
. -
Moved Cassandra exceptions
o.s.d.c.support.exception
too.s.d.cassandra
. -
Moved
o.s.d.c.convert
too.s.d.c.core.convert
(affects converters). -
Moved
o.s.d.c.mapping
too.s.d.c.core.mapping
(affects mapping annotations). -
Moved
MapId
fromo.s.d.c.repository
too.s.d.c.core.mapping
.
Revised CqlTemplate
/CassandraTemplate
We split CqlTemplate
and CassandraTemplate
in three ways:
-
CassandraTemplate
is no longer aCqlTemplate
but uses an instance that allows reuse and fine-grained control over fetch size, consistency levels, and retry policies. You can obtain theCqlOperations
throughCassandraTemplate.getCqlOperations()
. Because of the change, dependency injection ofCqlTemplate
requires additional bean setup. -
CqlTemplate
now reflects basic CQL operations instead of mixing high-level and low-level API calls (such ascount(…)
versusexecute(…)
) and the reduced method set is aligned with Spring Frameworks’sJdbcTemplate
with its convenient callback interfaces. -
Asynchronous methods are re-implemented on
AsyncCqlTemplate
andAsyncCassandraTemplate
by usingListenableFuture
. We removedCancellable
and the various async callback listeners.ListenableFuture
is a flexible approach and allows transition into aCompletableFuture
.
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
toMapIdCassandraRepository
-
Renamed
TypedIdCassandraRepository
toCassandraRepository
-
Introduced
TypedIdCassandraRepository
, extendingCassandraRepository
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.
If you’re using XML-based configuration, make sure to migrate all configuration files from the cql
namespace (http://www.springframework.org/schema/cql https://www.springframework.org/schema/cql/spring-cql.xsd
) to the cassandra
namespace (http://www.springframework.org/schema/data/cassandra https://www.springframework.org/schema/data/cassandra/spring-cassandra.xsd
).
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.
<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>
<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.
<cql:template session-ref="…" />
<cassandra:template session-ref="…" cassandra-converter-ref="…"/>
<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
orTupleValue
@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 asDefaultConsistencyLevel
.@Consistency
was adapted toDefaultConsistencyLevel
. -
RetryPolicy
onQueryOptions
and…CqlTemplate
types was removed without replacement. -
Drivers’s
PagingState
type was removed. Paging state now usesByteBuffer
. -
SimpleUserTypeResolver
acceptsCqlSession
instead ofCluster
. -
SimpleTupleTypeFactory
was migrated toenum
.SimpleTupleTypeFactory.INSTANCE
no longer requires aCluster
/CqlSession
context. -
Introduction of
StatementBuilder
to functionally build statements as the QueryBuilder API uses immutable statement types. -
Session
bean renamed fromsession
tocassandraSession
andSessionFactory
bean renamed fromsessionFactory
tocassandraSessionFactory
. -
ReactiveSession
bean renamed fromreactiveSession
toreactiveCassandraSession
andReactiveSessionFactory
bean renamed fromreactiveSessionFactory
toreactiveCassandraSessionFactory
. -
ReactiveSessionFactory.getSession()
now returns aMono<ReactiveSession>
. Previously it returned justReactiveSession
. -
Data type resolution was moved into
ColumnTypeResolver
so allDataType
-related methods were moved fromCassandraPersistentEntity
/CassandraPersistentProperty
intoColumnTypeResolver
(affected methods areMappingContext.getDataType(…)
,CassandraPersistentProperty.getDataType()
,CassandraPersistentEntity.getUserType()
, andCassandraPersistentEntity.getTupleType()
). -
Schema creation was moved from
MappingContext
toSchemaFactory
(affected methods areCassandraMappingContext.getCreateTableSpecificationFor(…)
,CassandraMappingContext.getCreateIndexSpecificationsFor(…)
, andCassandraMappingContext.getCreateUserTypeSpecificationFor(…)
).
Deprecations
-
CassandraCqlSessionFactoryBean
, useCqlSessionFactoryBean
instead. -
KeyspaceIdentifier
andCqlIdentifier
, usecom.datastax.oss.driver.api.core.CqlIdentifier
instead. -
CassandraSessionFactoryBean
, useCqlSessionFactoryBean
instead. -
AbstractCqlTemplateConfiguration
, useAbstractSessionConfiguration
instead. -
AbstractSessionConfiguration.getClusterName()
, useAbstractSessionConfiguration.getSessionName()
instead. -
CodecRegistryTupleTypeFactory
, useSimpleTupleTypeFactory
instead. -
Spring Data’s
CqlIdentifier
, use the driverCqlIdentifier
instead. -
forceQuote
attributes as quoting is no longer required.CqlIdentifier
properly escapes reserved keywords and takes care of case-sensitivity. -
fetchSize
onQueryOptions
and…CqlTemplate
types was deprecated, usepageSize
instead -
CassandraMappingContext.setUserTypeResolver(…)
,CassandraMappingContext.setCodecRegistry(…)
, andCassandraMappingContext.setCustomConversions(…)
: Configure these properties onCassandraConverter
. -
TupleTypeFactory
andCassandraMappingContext.setTupleTypeFactory(…)
:TupleTypeFactory
is no longer used as the Cassandra driver ships with aDataTypes.tupleOf(…)
factory method. -
Schema creation via
CqlSessionFactoryBean
(cassandra:session
) is deprecated. Keyspace creation viaCqlSessionFactoryBean
(cassandra:session
) is not affected.
Removals
Configuration API
-
PoolingOptionsFactoryBean
-
SocketOptionsFactoryBean
-
CassandraClusterFactoryBean
-
CassandraClusterParser
-
CassandraCqlClusterFactoryBean
-
CassandraCqlClusterParser
-
CassandraCqlSessionParser
-
AbstractClusterConfiguration
-
ClusterBuilderConfigurer
(useSessionBuilderConfigurer
instead
Utilities
-
GuavaListenableFutureAdapter
-
QueryOptions
andWriteOptions
constructor takingConsistencyLevel
andRetryPolicy
arguments. Use the builder in conjunction of execution profiles as replacement. -
CassandraAccessor.setRetryPolicy(…)
andReactiveCqlTemplate.setRetryPolicy(…)
methods. Use execution profiles as replacement.
Namespace support
-
cql
namespace (http://www.springframework.org/schema/cql
, usehttp://www.springframework.org/schema/data/cassandra
instead) -
cassandra:cluster
(endpoint properties merged tocassandra:session
) -
cql:template
, usecassandra:cql-template
instead -
Removed implicit bean registrations Mapping Context, Context and Template API beans. These must be declared explicitly.
Additions
Configuration API
-
CqlSessionFactoryBean
-
InitializeKeyspaceBeanDefinitionParser
-
SessionFactoryFactoryBean
including schema creation viaKeyspacePopulator
-
KeyspacePopulator
andSessionFactoryInitializer
to initialize a keyspace
Namespace support
-
cassandra:cluster
(endpoint properties merged tocassandra:session
) -
cassandra:initialize-keyspace
namespace support -
cassandra:session-factory
withcassandra:script
support