In this blog, we will learn Spring Boot integration with NoSQL database Apache Cassandra with the help of a working example, along with Python and Apache Cassandra installation steps on Windows OS.
Also, we will discuss in details – What is the Cassandra database used for? What makes Apache Cassandra unique? Who should use Cassandra? and lightweight transactions feature provided by cassandra.
First of all an overview on Application architecture.
In this article we will be exposing the Rest API for the below operations to the enduser in Spring Boot using @RestController. For Example:
/customer/save - insert records in Apache Cassandra database.
/customer/findall - fetch all the records from Apache Cassandra database.
/customer/findbyfirstname/firstname={firstname} - select a particular record from Apache Cassandra database having {firstname}.
/customer/findbyagegreaterthan/age={age} - retrieve records from Apache Cassandra database for specific cutomers having age greater than {age}.
/customer/findbyid/id={id} - retrieve records from Apache Cassandra database for specific {customer id}. E.g. id=2
/customer/deleteall - clear all the records from Apache Cassandra database permanantely.
Here is the high level application’s architecture with Apache Cassandra integration.
Yes, it’s NoSQL etc. We all know this, but according to the question – with Cassandra, we can achieve super fast writes and reads operations in a distributed high available ecosphere, if you use it the correct way.
Note: If you want to update or delete a lot of objects you should not build on Cassandra. Updates and Deletes lead to massive creation of tombstones which will lead to massive garbage collecting. This will decrease performance, especially for reads.
I would like to highlight an important finding with you.
A lightweight transactions feature – At some point Cassandra came with some new features, e.g. “lightweight transactions” right?. This sounds like a great tool at first but it comes with a big tradeoff. Think of something like “INSERT … IF NOT EXISTS” in a 100 node cluster with a quorum of 5 or so. This means your call will go round to 5 servers to check if any of them has a value righ? This is the anti-pattern we can say. Do not use this functionality! There’s SQL for such things. I don’t understand why they put time into developing this. If you have clarity on this please drop your comment in comment section, it can help others.
Note: Apache Cassandra requires Java 8 to run on a Windows OS. Additionally, the Cassandra Command-Line shell (cqlsh) is dependent on Python 2.7 to work correctly.
Hence, in this blog we will focus on
We need to install Python 2.7 for cqlsh. Url : https://www.python.org/downloads/release/python-2718/
Click on Next> button.
Click on Next> button.
Click on Next> button.
Note: Once Python 2.7.18 installed on Windows 10 OS, we need to set environmental variables otherwise Apache Casandra cqlsh will not be prompted.
We can download Apache Cassandra from the Official website: Url: https://cassandra.apache.org/download/
Unzip the compressed tar.gz folder using a compression tool such as 7-Zip or WinZip.
Set up the environment variables for Apache Cassandra to enable the database to interact with our application and operate on Windows as cqlsh.
Navigate to the Cassandra bin folder. Start the Windows Command Prompt directly from within the bin folder by typing cmd in the address bar and pressing Enter.
Enter the following command to access the Cassandra cqlsh bash shell
cqlsh
For running out Spring Boot microservice we need to create keyspace-name, table and index on table in Cassandra through cqlsh bash shell. Here, we are creating a keyspace named bootdb along with table customer having id as primary key.
======== cqlsh commands ===================================
cqlsh> create keyspace bootdb with replication={'class':'SimpleStrategy', 'replication_factor':1};
cqlsh> use bootdb;
cqlsh:bootdb> create table customer(id int primary key, firstanme text, lastname text, age int);
cqlsh:bootdb> create index on bootdb.customer(firstanme);
cqlsh:bootdb>
Java 8
Maven 3.5.0
Spring Boot 2.4.2
Eclipse
Spring Boot Starter Data Cassandra
Spring Boot Rest API
Apache Cassandra 3.11.10
Project’s maven file pom.xml would be looked like as below.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>spring-boot-data-cassandra</artifactId>
<version>0.0.1</version>
<packaging>jar</packaging>
<name>spring-boot-data-cassandra</name>
<description>Spring Boot Data Cassandra </description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-cassandra</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
spring.data.cassandra.keyspace-name=bootdb
spring.data.cassandra.schema-act=create_if_not_exists
spring.data.cassandra.contact-points=datacenter1
spring.data.cassandra.port=9042
#app config
server.port=8080
spring.application.name=SpringBootCassandra
server.servlet.context-path=/customer
Note: We need to configure contact-points as datacenter1 otherwise it will throw connection exception.
package com.the.basic.tech.info.cassandra;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringDataCassandraApplication {
public static void main(String[] args) {
SpringApplication.run(SpringDataCassandraApplication.class, args);
}
}
In this java class we will expose REST APIs using @RestController annotations as below.
package com.the.basic.tech.info.cassandra.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.the.basic.tech.info.cassandra.model.Customer;
import com.the.basic.tech.info.cassandra.repository.CustomerRepository;
@RestController
@RequestMapping(value = "/")
public class CustomerController {
@Autowired
private CustomerRepository customerRepository;
@RequestMapping("/save")
public String save() {
Customer cust_1 = new Customer(1, "Peter", "Smith", 20);
Customer cust_2 = new Customer(2, "Mary", "Taylor", 25);
Customer cust_3 = new Customer(3, "Peter", "Brown", 30);
Customer cust_4 = new Customer(4, "Lauren", "Taylor", 20);
Customer cust_5 = new Customer(5, "Lauren", "Flores", 45);
Customer cust_6 = new Customer(6, "Peter", "Williams", 20);
// save customers to ElasticSearch
customerRepository.save(cust_1);
customerRepository.save(cust_2);
customerRepository.save(cust_3);
customerRepository.save(cust_4);
customerRepository.save(cust_5);
customerRepository.save(cust_6);
return "Save Operation Executed Successfully. Please call findall API.";
}
@RequestMapping("/findall")
public String findAll() {
String result = "";
List<Customer> customers = customerRepository.findAll();
if (customers.size() != 0) {
for (Customer customer : customers) {
result += customer.toString() + "<br>";
}
} else {
result = "No Record Found!";
}
return result;
}
@RequestMapping("/findbyfirstname")
public String findByFirstName(@RequestParam("firstname") String firstname) {
List<Customer> customers = customerRepository.findByFirstname(firstname);
String result = "";
for (Customer customer : customers) {
result += customer.toString() + "<br>";
}
return result;
}
@RequestMapping("/findbyagegreaterthan")
public String findByAgeGreaterThan(@RequestParam("age") int age) {
List<Customer> customers = customerRepository.findCustomerHasAgeGreaterThan(age);
String result = "";
for (Customer customer : customers) {
result += customer.toString() + "<br>";
}
return result;
}
@RequestMapping("/findbyid")
public String findById(@RequestParam("id") int id) {
List<Customer> customers = customerRepository.findById(id);
String result = "";
for (Customer customer : customers) {
result += customer.toString() + "<br>";
}
return result;
}
@RequestMapping("/deleteall")
public String deleteAll() {
customerRepository.deleteAll();
return "Delete Operation Executed Successfully. Please call findall API.";
}
}
We are adding Customer entity for mapping the attributes.
package com.the.basic.tech.info.cassandra.model;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.Table;
@Table
public class Customer {
@PrimaryKey
private int id;
private String firstanme;
private String lastname;
private int age;
public Customer(){}
public Customer(int id, String firstname, String lastname, int age){
this.id = id;
this.firstanme = firstname;
this.lastname = lastname;
this.age = age;
}
public void setId(int id){
this.id = id;
}
public int getId(){
return this.id;
}
public void setFirstname(String firstname){
this.firstanme = firstname;
}
public String getFirstname(){
return this.firstanme;
}
public void setLastname(String lastname){
this.lastname = lastname;
}
public String getLastname(){
return this.lastname;
}
public void setAge(int age){
this.age = age;
}
public int getAge(){
return this.age;
}
@Override
public String toString() {
return String.format("Customer[id=%d, firstName='%s', lastName='%s', age=%d]", this.id, this.firstanme, this.lastname, this.age);
}
}
We need to extends CrudRepository Interface as shown below. Basically here we are writing our DB Queries.
package com.the.basic.tech.info.cassandra.repository;
import java.util.List;
import org.springframework.data.cassandra.repository.Query;
import org.springframework.data.repository.CrudRepository;
import com.the.basic.tech.info.cassandra.model.Customer;
public interface CustomerRepository extends CrudRepository<Customer, String> {
@Query(value="SELECT * FROM customer WHERE firstanme=?0")
public List<Customer> findByFirstname(String firstname);
@Query("SELECT * FROM customer WHERE age > ?0 ALLOW FILTERING")
public List<Customer> findCustomerHasAgeGreaterThan(int age);
@Query("SELECT * FROM customer")
public List<Customer> findAll();
@Query("SELECT * FROM customer WHERE id =?0")
public List<Customer> findById(int id);
}
Run spring-boot-data-cassandra: mvn spring-boot:run
D:\development\spring-boot-data-cassandra>mvn spring-boot:run
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building spring-boot-data-cassandra 0.0.1
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] >>> spring-boot-maven-plugin:2.4.2:run (default-cli) > test-compile @ spring-boot-data-cassandra >>>
[INFO]
[INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ spring-boot-data-cassandra ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 1 resource
[INFO] Copying 1 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ spring-boot-data-cassandra ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-resources-plugin:3.2.0:testResources (default-testResources) @ spring-boot-data-cassandra ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] skip non existing resourceDirectory D:\development\spring-boot-data-cassandra\src\test\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ spring-boot-data-cassandra ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] <<< spring-boot-maven-plugin:2.4.2:run (default-cli) < test-compile @ spring-boot-data-cassandra <<<
[INFO]
[INFO]
[INFO] --- spring-boot-maven-plugin:2.4.2:run (default-cli) @ spring-boot-data-cassandra ---
[INFO] Attaching agents: []
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.4.2)
2021-07-17 00:07:53.712 INFO 25584 --- [ main] t.b.t.i.c.SpringDataCassandraApplication : Starting SpringDataCassandraApplication using Java 1.8.0_212 on LOCALHOST with PID 25584 (D:\development\spring-boot-data-cassandra\target\classes started by 172025 in D:\development\spring-boot-data-cassandra)
2021-07-17 00:07:53.718 INFO 25584 --- [ main] t.b.t.i.c.SpringDataCassandraApplication : No active profile set, falling back to default profiles: default
2021-07-17 00:07:54.526 INFO 25584 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Reactive Cassandra repositories in DEFAULT mode.
2021-07-17 00:07:54.585 INFO 25584 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 46 ms. Found 0 Reactive Cassandra repository interfaces.
2021-07-17 00:07:54.593 INFO 25584 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Cassandra repositories in DEFAULT mode.
2021-07-17 00:07:54.625 INFO 25584 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 31 ms. Found 1 Cassandra repository interfaces.
2021-07-17 00:07:56.009 INFO 25584 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2021-07-17 00:07:56.026 INFO 25584 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2021-07-17 00:07:56.026 INFO 25584 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.41]
2021-07-17 00:07:56.232 INFO 25584 --- [ main] o.a.c.c.C.[.[localhost].[/customer] : Initializing Spring embedded WebApplicationContext
2021-07-17 00:07:56.232 INFO 25584 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2439 ms
2021-07-17 00:08:03.355 INFO 25584 --- [ main] c.d.o.d.i.core.DefaultMavenCoordinates : DataStax Java driver for Apache Cassandra(R) (com.datastax.oss:java-driver-core) version 4.9.0
2021-07-17 00:10:17.918 INFO 25584 --- [ s0-admin-0] c.d.oss.driver.internal.core.time.Clock : Using native clock for microsecond precision
2021-07-17 00:10:17.922 INFO 25584 --- [ s0-admin-0] c.d.o.d.i.core.metadata.MetadataManager : [s0] No contact points provided, defaulting to /127.0.0.1:9042
2021-07-17 00:10:20.728 INFO 25584 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2021-07-17 00:10:20.991 INFO 25584 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '/customer'
2021-07-17 00:10:21.014 INFO 25584 --- [ main] t.b.t.i.c.SpringDataCassandraApplication : Started SpringDataCassandraApplication in 148.002 seconds (JVM running for 148.846)
2021-07-17 00:10:30.210 INFO 25584 --- [nio-8080-exec-1] o.a.c.c.C.[.[localhost].[/customer] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-07-17 00:10:30.212 INFO 25584 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2021-07-17 00:10:30.218 INFO 25584 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 2 ms
We can see Truncate logs here.
INFO [main] 2021-07-17 00:07:29,457 StorageService.java:1536 - JOINING: Finish joining ring
INFO [main] 2021-07-17 00:07:29,521 SecondaryIndexManager.java:512 - Executing pre-join tasks for: CFS(Keyspace='bootdb', ColumnFamily='customer')
INFO [main] 2021-07-17 00:07:29,601 StorageService.java:2452 - Node localhost/127.0.0.1 state jump to NORMAL
INFO [main] 2021-07-17 00:07:30,121 NativeTransportService.java:73 - Netty using Java NIO event loop
INFO [main] 2021-07-17 00:07:30,373 Server.java:158 - Using Netty Version: [netty-buffer=netty-buffer-4.0.44.Final.452812a, netty-codec=netty-codec-4.0.44.Final.452812a, netty-codec-haproxy=netty-codec-haproxy-4.0.44.Final.452812a, netty-codec-http=netty-codec-http-4.0.44.Final.452812a, netty-codec-socks=netty-codec-socks-4.0.44.Final.452812a, netty-common=netty-common-4.0.44.Final.452812a, netty-handler=netty-handler-4.0.44.Final.452812a, netty-tcnative=netty-tcnative-1.1.33.Fork26.142ecbb, netty-transport=netty-transport-4.0.44.Final.452812a, netty-transport-native-epoll=netty-transport-native-epoll-4.0.44.Final.452812a, netty-transport-rxtx=netty-transport-rxtx-4.0.44.Final.452812a, netty-transport-sctp=netty-transport-sctp-4.0.44.Final.452812a, netty-transport-udt=netty-transport-udt-4.0.44.Final.452812a]
INFO [main] 2021-07-17 00:07:30,377 Server.java:159 - Starting listening for CQL clients on localhost/127.0.0.1:9042 (unencrypted)...
INFO [main] 2021-07-17 00:07:31,516 CassandraDaemon.java:564 - Not starting RPC server as requested. Use JMX (StorageService->startRPCServer()) or nodetool (enablethrift) to start it
INFO [main] 2021-07-17 00:07:31,517 CassandraDaemon.java:650 - Startup complete
INFO [Native-Transport-Requests-1] 2021-07-17 00:12:45,963 OutboundTcpConnection.java:108 - OutboundTcpConnection using coalescing strategy DISABLED
INFO [HANDSHAKE-localhost/127.0.0.1] 2021-07-17 00:12:46,020 OutboundTcpConnection.java:561 - Handshaking version with localhost/127.0.0.1
INFO [MutationStage-2] 2021-07-17 00:12:46,041 ColumnFamilyStore.java:2221 - Truncating bootdb.customer
INFO [MutationStage-2] 2021-07-17 00:12:46,352 ColumnFamilyStore.java:2270 - Truncate of bootdb.customer is complete
INFO [MutationStage-2] 2021-07-17 00:32:03,826 ColumnFamilyStore.java:2221 - Truncating bootdb.customer
INFO [MutationStage-2] 2021-07-17 00:32:04,230 ColumnFamilyStore.java:2270 - Truncate of bootdb.customer is complete
Have a great Day :). Drop your question in the comment box if any.