Cassandra
Module provides a repository implementation for the Cassandra database using the DataStax driver.
Cassandra is a distributed column-oriented database where queries are written in CQL, and the data model is usually designed around specific read scenarios.
In Kora, the Cassandra module provides declarative repositories on top of CqlSession: the application writes CQL queries in @Query, and Kora generates query preparation, parameter binding, and result mapping code at compile time.
Common rules for entities, @Repository, @Query, macros, batch queries, and the @Table, @Column, @Id, @Embedded annotations are described in the common database section.
This document covers the Cassandra-specific parts: driver connection, CqlSession configuration, execution profiles, UDT, mappers, and supported method signatures.
For a step-by-step walkthrough before the reference details, see Cassandra Database.
Dependency¶
Dependency build.gradle:
Module:
Dependency build.gradle.kts:
Module:
Configuration¶
Configuration is read from the cassandra section and described by the CassandraConfig interface.
At minimum, basic.contactPoints must be specified. Other parameters are optional or passed to the driver only when explicitly configured.
Simple configuration example:
cassandra {
basic {
contactPoints = "127.0.0.1:9042, 127.0.0.2:9042" //(1)!
dc = "datacenter1" //(2)!
sessionKeyspace = "test-db" //(3)!
request {
timeout = "5s" //(4)!
}
}
auth {
login = "username" //(5)!
password = "password" //(6)!
}
}
Cassandranode addresses for connecting to the database (required, no default)Cassandradatacenter name (not specified by default, optional)keyspacename for the connection (not specified by default, optional)- Query execution timeout for the connection (not specified by default, optional)
- Username for the connection (not specified by default, optional)
- Password for the connection (not specified by default, optional)
cassandra:
basic:
contactPoints: "127.0.0.1:9042, 127.0.0.2:9042" #(1)!
dc: "datacenter1" #(2)!
sessionKeyspace: "test-db" #(3)!
request:
timeout: "5s" #(4)!
auth:
login: "username" #(5)!
password: "password" #(6)!
Cassandranode addresses for connecting to the database (required, no default)Cassandradatacenter name (not specified by default, optional)keyspacename for the connection (not specified by default, optional)- Query execution timeout for the connection (not specified by default, optional)
- Username for the connection (not specified by default, optional)
- Password for the connection (not specified by default, optional)
Full configuration example
Full configuration with example values. Parameter descriptions are shared by the HOCON and YAML examples.
cassandra {
auth {
login = "username" //(1)!
password = "password" //(2)!
}
basic {
contactPoints = [ "127.0.0.1:9042", "127.0.0.2:9042" ] //(3)!
sessionName = "some-session-name" //(4)!
dc = "datacenter1" //(5)!
sessionKeyspace = "test-db" //(6)!
loadBalancingPolicy.slowReplicaAvoidance = true //(7)!
cloud.secureConnectBundle = "/location/of/secure/connect/bundle" //(8)!
request {
timeout = "5s" //(9)!
consistency = "LOCAL_ONE" //(10)!
pageSize = 5000 //(11)!
serialConsistency = "LOCAL_SERIAL" //(12)!
defaultIdempotence = false //(13)!
}
}
advanced {
sessionLeak.threshold = 4 //(14)!
connection {
connectTimeout = "10s" //(15)!
initQueryTimeout = "10s" //(16)!
setKeyspaceTimeout = "10s" //(17)!
maxRequestsPerConnection = 1024 //(18)!
maxOrphanRequests = 256 //(19)!
warnOnInitError = true //(20)!
pool {
localSize = 10 //(21)!
remoteSize = 10 //(22)!
}
}
reconnectOnInit = false //(23)!
reconnectionPolicy {
baseDelay = "1s" //(24)!
maxDelay = "60s" //(25)!
}
loadBalancingPolicy.dcFailover {
maxNodesPerRemoveDc = 1 //(26)!
allowForLocalConsistencyLevels = false //(27)!
}
sslEngineFactory {
cipherSuites = [ "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA" ] //(28)!
hostnameValidation = true //(29)!
keystorePath = "/path/to/client.keystore" //(30)!
keystorePassword = "password" //(31)!
truststorePath = "/path/to/client.truststore" //(32)!
truststorePassword = "password" //(33)!
}
timestampGenerator {
forceJavaClock = false //(34)!
driftWarning.threshold = "1s" //(35)!
driftWarning.interval = "10s" //(36)!
}
protocol {
version = "V4" //(37)!
compression = "lz4" //(38)!
maxFrameLength = 268435456 //(39)!
}
request {
warnIfSetKeyspace = true //(40)!
trace {
attempts = 5 //(41)!
interval = "1ms" //(42)!
consistency = "ONE" //(43)!
}
logWarnings = true //(44)!
}
metrics {
idGenerator {
name = "TaggingMetricIdGenerator" //(45)!
prefix = "my-app" //(46)!
}
node {
enabled = [ "bytes-sent", "bytes-received", "open-connections" ] //(47)!
cqlMessages {
lowestLatency = "1ms" //(48)!
highestLatency = "90s" //(49)!
significantDigits = 1 //(50)!
refreshInterval = "10s" //(51)!
slo = [ 1, 10, 50, 100, 200, 500, 1000 ] //(52)!
}
}
session {
enabled = [ "connected-nodes", "cql-requests", "cql-client-timeouts" ] //(53)!
cqlRequests {
lowestLatency = "1ms" //(54)!
highestLatency = "90s" //(55)!
significantDigits = 1 //(56)!
refreshInterval = "10s" //(57)!
slo = [ 1, 10, 50, 100, 200, 500, 1000 ] //(58)!
}
throttlingDelay {
lowestLatency = "1ms" //(59)!
highestLatency = "90s" //(60)!
significantDigits = 1 //(61)!
refreshInterval = "10s" //(62)!
slo = [ 1, 10, 50, 100, 200, 500, 1000 ] //(63)!
}
}
publishPercentileHistogram = false //(64)!
}
socket {
tcpNoDelay = true //(65)!
keepAlive = false //(66)!
reuseAddress = true //(67)!
lingerInterval = 0 //(68)!
receiveBufferSize = 65535 //(69)!
sendBufferSize = 65535 //(70)!
}
heartbeat {
interval = "30s" //(71)!
timeout = "2m" //(72)!
}
metadata {
schema {
enabled = true //(73)!
requestTimeout = "20s" //(74)!
requestPageSize = 20 //(75)!
refreshedKeyspaces = [ "ks1", "ks2" ] //(76)!
debouncer.window = "1s" //(77)!
debouncer.maxEvents = 20 //(78)!
}
topologyEventDebouncer.window = "1s" //(79)!
topologyEventDebouncer.maxEvents = 20 //(80)!
tokenMapEnabled = true //(81)!
}
controlConnection {
timeout = "10s" //(82)!
schemaAgreement {
interval = "200ms" //(83)!
timeout = "10s" //(84)!
warnOnFailure = true //(85)!
}
}
preparedStatements {
prepareOnAllNodes = true //(86)!
reprepareOnUp {
enabled = true //(87)!
checkSystemTable = false //(88)!
maxStatements = 0 //(89)!
maxParallelism = 100 //(90)!
timeout = "20s" //(91)!
}
preparedCache.weakValues = false //(92)!
}
netty {
ioGroup.size = 0 //(93)!
ioGroup.shutdown {
quietPeriod = 2 //(94)!
timeout = 15 //(95)!
unit = "SECONDS" //(96)!
}
adminGroup.size = 2 //(97)!
adminGroup.shutdown {
quietPeriod = 2 //(98)!
timeout = 15 //(99)!
unit = "SECONDS" //(100)!
}
timer.tickDuration = "100ms" //(101)!
timer.ticksPerWheel = 2048 //(102)!
daemon = false //(103)!
}
coalescer.rescheduleInterval = "10ms" //(104)!
resolveContactPoints = false //(105)!
throttler {
throttlerClass = "ConcurrencyLimitingRequestThrottler" //(106)!
maxConcurrentRequests = 1024 //(107)!
maxRequestsPerSecond = 10000 //(108)!
maxQueueSize = 10000 //(109)!
drainInterval = "1ms" //(110)!
}
}
profiles {
someProfile {
basic.request.timeout = "10s" //(111)!
basic.request.consistency = "LOCAL_QUORUM" //(112)!
advanced.request.trace.attempts = 3 //(113)!
advanced.request.trace.consistency = "ONE" //(114)!
}
}
telemetry {
logging.enabled = false //(115)!
metrics {
enabled = true //(116)!
slo = [ 1, 10, 50, 100, 200, 500, 1000 ] //(117)!
tags = { "key1" = "value1", "key2" = "value2" } //(118)!
}
tracing {
enabled = true //(119)!
attributes = { "key1" = "value1", "key2" = "value2" } //(120)!
}
}
}
cassandra:
auth:
login: "username" #(1)!
password: "password" #(2)!
basic:
contactPoints: [ "127.0.0.1:9042", "127.0.0.2:9042" ] #(3)!
sessionName: "some-session-name" #(4)!
dc: "datacenter1" #(5)!
sessionKeyspace: "test-db" #(6)!
loadBalancingPolicy:
slowReplicaAvoidance: true #(7)!
cloud:
secureConnectBundle: "/location/of/secure/connect/bundle" #(8)!
request:
timeout: "5s" #(9)!
consistency: "LOCAL_ONE" #(10)!
pageSize: 5000 #(11)!
serialConsistency: "LOCAL_SERIAL" #(12)!
defaultIdempotence: false #(13)!
advanced:
sessionLeak:
threshold: 4 #(14)!
connection:
connectTimeout: "10s" #(15)!
initQueryTimeout: "10s" #(16)!
setKeyspaceTimeout: "10s" #(17)!
maxRequestsPerConnection: 1024 #(18)!
maxOrphanRequests: 256 #(19)!
warnOnInitError: true #(20)!
pool:
localSize: 10 #(21)!
remoteSize: 10 #(22)!
reconnectOnInit: false #(23)!
reconnectionPolicy:
baseDelay: "1s" #(24)!
maxDelay: "60s" #(25)!
loadBalancingPolicy:
dcFailover:
maxNodesPerRemoveDc: 1 #(26)!
allowForLocalConsistencyLevels: false #(27)!
sslEngineFactory:
cipherSuites: [ "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA" ] #(28)!
hostnameValidation: true #(29)!
keystorePath: "/path/to/client.keystore" #(30)!
keystorePassword: "password" #(31)!
truststorePath: "/path/to/client.truststore" #(32)!
truststorePassword: "password" #(33)!
timestampGenerator:
forceJavaClock: false #(34)!
driftWarning:
threshold: "1s" #(35)!
interval: "10s" #(36)!
protocol:
version: "V4" #(37)!
compression: "lz4" #(38)!
maxFrameLength: 268435456 #(39)!
request:
warnIfSetKeyspace: true #(40)!
trace:
attempts: 5 #(41)!
interval: "1ms" #(42)!
consistency: "ONE" #(43)!
logWarnings: true #(44)!
metrics:
idGenerator:
name: "TaggingMetricIdGenerator" #(45)!
prefix: "my-app" #(46)!
node:
enabled: [ "bytes-sent", "bytes-received", "open-connections" ] #(47)!
cqlMessages:
lowestLatency: "1ms" #(48)!
highestLatency: "90s" #(49)!
significantDigits: 1 #(50)!
refreshInterval: "10s" #(51)!
slo: [ 1, 10, 50, 100, 200, 500, 1000 ] #(52)!
session:
enabled: [ "connected-nodes", "cql-requests", "cql-client-timeouts" ] #(53)!
cqlRequests:
lowestLatency: "1ms" #(54)!
highestLatency: "90s" #(55)!
significantDigits: 1 #(56)!
refreshInterval: "10s" #(57)!
slo: [ 1, 10, 50, 100, 200, 500, 1000 ] #(58)!
throttlingDelay:
lowestLatency: "1ms" #(59)!
highestLatency: "90s" #(60)!
significantDigits: 1 #(61)!
refreshInterval: "10s" #(62)!
slo: [ 1, 10, 50, 100, 200, 500, 1000 ] #(63)!
publishPercentileHistogram: false #(64)!
socket:
tcpNoDelay: true #(65)!
keepAlive: false #(66)!
reuseAddress: true #(67)!
lingerInterval: 0 #(68)!
receiveBufferSize: 65535 #(69)!
sendBufferSize: 65535 #(70)!
heartbeat:
interval: "30s" #(71)!
timeout: "2m" #(72)!
metadata:
schema:
enabled: true #(73)!
requestTimeout: "20s" #(74)!
requestPageSize: 20 #(75)!
refreshedKeyspaces: [ "ks1", "ks2" ] #(76)!
debouncer:
window: "1s" #(77)!
maxEvents: 20 #(78)!
topologyEventDebouncer:
window: "1s" #(79)!
maxEvents: 20 #(80)!
tokenMapEnabled: true #(81)!
controlConnection:
timeout: "10s" #(82)!
schemaAgreement:
interval: "200ms" #(83)!
timeout: "10s" #(84)!
warnOnFailure: true #(85)!
preparedStatements:
prepareOnAllNodes: true #(86)!
reprepareOnUp:
enabled: true #(87)!
checkSystemTable: false #(88)!
maxStatements: 0 #(89)!
maxParallelism: 100 #(90)!
timeout: "20s" #(91)!
preparedCache:
weakValues: false #(92)!
netty:
ioGroup:
size: 0 #(93)!
shutdown:
quietPeriod: 2 #(94)!
timeout: 15 #(95)!
unit: "SECONDS" #(96)!
adminGroup:
size: 2 #(97)!
shutdown:
quietPeriod: 2 #(98)!
timeout: 15 #(99)!
unit: "SECONDS" #(100)!
timer:
tickDuration: "100ms" #(101)!
ticksPerWheel: 2048 #(102)!
daemon: false #(103)!
coalescer:
rescheduleInterval: "10ms" #(104)!
resolveContactPoints: false #(105)!
throttler:
throttlerClass: "ConcurrencyLimitingRequestThrottler" #(106)!
maxConcurrentRequests: 1024 #(107)!
maxRequestsPerSecond: 10000 #(108)!
maxQueueSize: 10000 #(109)!
drainInterval: "1ms" #(110)!
profiles:
someProfile:
basic:
request:
timeout: "10s" #(111)!
consistency: "LOCAL_QUORUM" #(112)!
advanced:
request:
trace:
attempts: 3 #(113)!
consistency: "ONE" #(114)!
telemetry:
logging:
enabled: false #(115)!
metrics:
enabled: true #(116)!
slo: [ 1, 10, 50, 100, 200, 500, 1000 ] #(117)!
tags: { key1: "value1", key2: "value2" } #(118)!
tracing:
enabled: true #(119)!
attributes: { key1: "value1", key2: "value2" } #(120)!
- Username for authentication in
Cassandra(not specified by default, optional). - Password for authentication in
Cassandra(not specified by default, optional). Cassandranode addresses inhost:portformat (required, no default).- Driver session name used in logs, metrics, and diagnostics (not specified by default, optional).
- Local datacenter for the load-balancing policy (not specified by default, optional).
keyspacethat will be set for the session after connection (not specified by default, optional).- Enables slow replica avoidance in the default load-balancing policy (not specified by default, optional).
- Path or
URLto theSecure Connect Bundlefor connecting toDataStax Astra/ cloud Cassandra (not specified by default, optional). - Regular request timeout (not specified by default, optional).
- Regular request consistency level, for example
ONE,LOCAL_ONE,LOCAL_QUORUM,QUORUM,ALL(not specified by default, optional). - Result page size, meaning the maximum number of rows requested in one network round trip (not specified by default, optional).
- Serial consistency level for lightweight transactions
LWT:SERIALorLOCAL_SERIAL(not specified by default, optional). - Default request idempotence value; affects whether retries and speculative execution can be applied safely (not specified by default, optional).
- Driver session leak warning threshold (not specified by default, optional).
- Timeout for opening a network connection to a node (not specified by default, optional).
- Timeout for requests that the driver executes while initializing a connection (not specified by default, optional).
- Timeout for setting the
keyspaceon a connection (not specified by default, optional). - Maximum number of simultaneous requests per connection (not specified by default, optional).
- Maximum number of requests whose response is no longer awaited but may still complete inside the driver (not specified by default, optional).
- Logs a warning when connection initialization fails for an individual node (not specified by default, optional).
- Connection pool size for local datacenter nodes (not specified by default, optional).
- Connection pool size for remote nodes (not specified by default, optional).
- Allows initialization retry when all
contactPointsdo not answer during startup (not specified by default, optional). - Initial delay of the reconnection policy (not specified by default, optional).
- Maximum delay of the reconnection policy (not specified by default, optional).
- Maximum number of remote datacenter nodes that can be used for failover (not specified by default, optional).
- Allows failover to a remote datacenter for local consistency levels (not specified by default, optional).
- Allowed cipher suites for
SSL/TLS(not specified by default, optional). - Checks that the node hostname matches the
SSL/TLScertificate (not specified by default, optional). - Path to the client keystore (not specified by default, optional).
- Client keystore password (not specified by default, optional).
- Path to the truststore (not specified by default, optional).
- Truststore password (not specified by default, optional).
- Forces Java system clock usage for query timestamp generation (not specified by default, optional).
- Warning threshold for timestamp drift into the future (not specified by default, optional).
- Minimum interval between timestamp drift warnings (not specified by default, optional).
- Cassandra binary protocol version, for example
V4(not specified by default, optional). - Protocol compression algorithm, for example
lz4orsnappy(not specified by default, optional). - Maximum protocol frame size in bytes (not specified by default, optional).
- Logs a warning when a query explicitly changes the
keyspace(not specified by default, optional). - Number of attempts to fetch query tracing information from Cassandra (not specified by default, optional).
- Interval between attempts to fetch query tracing information (not specified by default, optional).
- Consistency level for queries to tracing tables (not specified by default, optional).
- Logs warnings returned by Cassandra with a query response (not specified by default, optional).
- Driver metric identifier generator name (default:
TaggingMetricIdGenerator). - Driver metric name prefix (not specified by default, optional).
- Enabled node-level metrics (default:
open-connections,in-flight,bytes-received,bytes-sent,write-timeouts,read-timeouts,aborted-requests). - Lowest expected latency for the
node.cqlMessagesmetric histogram (default:1ms). - Highest expected latency for the
node.cqlMessagesmetric histogram (default:90s). - Number of significant digits for the
node.cqlMessagesmetric histogram (not specified by default, optional). - Snapshot refresh interval for the
node.cqlMessagesmetric histogram (not specified by default, optional). SLOboundaries for thenode.cqlMessagesmetric (default:ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO).- Enabled session-level metrics (default:
connected-nodes,cql-requests,cql-client-timeouts,cql-prepared-cache-size,throttling.delay,throttling.queue-size). - Lowest expected latency for the
session.cqlRequestsmetric histogram (default:1ms). - Highest expected latency for the
session.cqlRequestsmetric histogram (default:90s). - Number of significant digits for the
session.cqlRequestsmetric histogram (not specified by default, optional). - Snapshot refresh interval for the
session.cqlRequestsmetric histogram (not specified by default, optional). SLOboundaries for thesession.cqlRequestsmetric (default:ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO).- Lowest expected latency for the
session.throttlingDelaymetric histogram (default:1ms). - Highest expected latency for the
session.throttlingDelaymetric histogram (default:90s). - Number of significant digits for the
session.throttlingDelaymetric histogram (not specified by default, optional). - Snapshot refresh interval for the
session.throttlingDelaymetric histogram (not specified by default, optional). SLOboundaries for thesession.throttlingDelaymetric (default:ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO).- Publishes percentile histograms for driver metrics (default:
false). - Enables
TCP_NODELAY, which disables Nagle's algorithm (not specified by default, optional). - Enables
SO_KEEPALIVEfor TCP sockets (not specified by default, optional). - Enables
SO_REUSEADDRfor TCP sockets (not specified by default, optional). SO_LINGERvalue for TCP sockets (not specified by default, optional).- TCP socket receive buffer size in bytes (not specified by default, optional).
- TCP socket send buffer size in bytes (not specified by default, optional).
- Interval for sending
heartbeaton an idle connection (not specified by default, optional). - Timeout for waiting for a
heartbeatresponse (not specified by default, optional). - Enables schema metadata loading and refresh (not specified by default, optional).
- Timeout for schema metadata queries (not specified by default, optional).
- Page size for schema metadata queries (not specified by default, optional).
- List of
keyspacenames whose schema metadata is refreshed by the driver (not specified by default, optional). - Window for coalescing schema refresh events before processing (not specified by default, optional).
- Maximum number of schema refresh events that can be accumulated in the window (not specified by default, optional).
- Window for coalescing cluster topology change events (not specified by default, optional).
- Maximum number of topology change events that can be accumulated in the window (not specified by default, optional).
- Enables the token map for routing requests by data owners (not specified by default, optional).
- Service
control connectiontimeout (not specified by default, optional). - Interval for checking
schema agreementbetween nodes (not specified by default, optional). - Maximum time to wait for
schema agreement(not specified by default, optional). - Logs a warning if
schema agreementis not reached in time (not specified by default, optional). - Prepares a statement on all nodes after it has been prepared successfully on one node (not specified by default, optional).
- Re-prepares statements on a node that became available again (not specified by default, optional).
- Checks the
system.prepared_statementssystem table before re-preparing a statement (not specified by default, optional). - Maximum number of statements to re-prepare;
0means no driver-side limit (not specified by default, optional). - Maximum number of parallel re-prepare requests (not specified by default, optional).
- Timeout for re-preparing statements on one node (not specified by default, optional).
- Stores prepared statement cache values through weak references (not specified by default, optional).
- Number of
Nettythreads for network I/O;0lets the driver choose automatically (not specified by default, optional). - Quiet period for graceful
ioGroupshutdown (not specified by default, optional). - Maximum wait time for
ioGroupshutdown (not specified by default, optional). - Unit for
ioGroupshutdown parameters (not specified by default, optional). - Number of
Nettythreads for driver administrative tasks (not specified by default, optional). - Quiet period for graceful
adminGroupshutdown (not specified by default, optional). - Maximum wait time for
adminGroupshutdown (not specified by default, optional). - Unit for
adminGroupshutdown parameters (not specified by default, optional). - Duration of one
Nettytimer tick for delayed driver tasks (not specified by default, optional). - Number of ticks in the
Nettytimer wheel (not specified by default, optional). - Makes
Nettythreads daemon threads (not specified by default, optional). - Rescheduling interval for message coalescing before sending (not specified by default, optional).
- Allows the driver to resolve
contactPointsthrough DNS during startup (not specified by default, optional). - Driver request throttler class (not specified by default, optional).
- Maximum number of concurrent requests for the throttler (not specified by default, optional).
- Maximum number of requests per second for the throttler (not specified by default, optional).
- Maximum throttler request queue size (not specified by default, optional).
- Interval at which the throttler releases requests from the queue (not specified by default, optional).
basic.request.timeoutoverride for thesomeProfileprofile (not specified by default, optional).basic.request.consistencyoverride for thesomeProfileprofile (not specified by default, optional).advanced.request.trace.attemptsoverride for thesomeProfileprofile (not specified by default, optional).advanced.request.trace.consistencyoverride for thesomeProfileprofile (not specified by default, optional).- Enables Kora query logging (default:
false). - Enables Kora query metrics (default:
true). - Kora metrics
SLOboundaries (default:ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO). - Additional Kora metric tags (default:
{}). - Enables Kora query tracing (default:
true). - Additional Kora tracing attributes (default:
{}).
Code configuration¶
You can configure the driver manually in your code by registering a CassandraConfigurer component.
The configure method receives the CqlSessionBuilder and the ProgrammaticDriverConfigLoaderBuilder,
so you can adjust the session builder and override raw driver options that are not exposed through the cassandra configuration section:
Usage¶
To create a repository, declare an interface with @Repository and extend CassandraRepository.
Such a repository gets access to CqlSession through generated code and uses @Query to execute CQL queries.
Query parameters are bound by name: :id, :entity.field, :filter.value.
Views are described with the common database annotations and marked with @EntityCassandra
so that Kora generates the view mapper at compile time (see View):
@Repository
public interface EntityRepository extends CassandraRepository {
@EntityCassandra
@Table("entities")
record Entity(@Id String id,
@Column("value1") int field1,
String value2,
@Nullable String value3) {}
@Query("SELECT %{return#selects} FROM %{return#table} WHERE id = :id") //(1)!
@Nullable
Entity findById(String id);
@Query("SELECT id, value1, value2, value3 FROM entities") //(2)!
List<Entity> findAll();
@Query("INSERT INTO %{entity#inserts}") //(3)!
void insert(Entity entity);
}
- Uses macros
%{return#selects}and%{return#table}. Expands to query: Method uses macros forSELECT. Details: Common Database Rules — Macros - Fields listed manually without macros — this is valid but requires maintenance when the view changes.
- Uses macro
%{entity#inserts}. Expands to query:Method uses macros forINSERT INTO entities(id, value1, value2, value3) VALUES(:entity.id, :entity.value1, :entity.value2, :entity.value3)INSERT. Details: Common Database Rules — Macros
@Repository
interface EntityRepository : CassandraRepository {
@EntityCassandra
@Table("entities")
data class Entity(
@field:Id val id: String,
@field:Column("value1") val field1: Int,
val value2: String,
val value3: String?
)
@Query("SELECT %{return#selects} FROM %{return#table} WHERE id = :id") //(1)!
fun findById(id: String): Entity?
@Query("INSERT INTO %{entity#inserts}") //(3)!
fun insert(entity: Entity)
}
- Uses macros
%{return#selects}and%{return#table}. Expands to query: Method uses macros forSELECT. Details: Common Database Rules — Macros - Fields listed manually without macros — this is valid but requires maintenance when the view changes.
- Uses macro
%{entity#inserts}. Expands to query:Method uses macros forINSERT INTO entities(id, value1, value2, value3) VALUES(:entity.id, :entity.value1, :entity.value2, :entity.value3)INSERT. Details: Common Database Rules — Macros
CQL remains under the developer's control: you write the query text yourself, while Kora only handles parameter binding,
query execution, and result mapping.
Common rules for entities, @Table, @Column, @Id, @Embedded, @Batch, and macros are described in
Common database rules.
Parameter binding: Kora performs typed injection of arguments into the CQL query at compile time.
Query parameters (e.g., :id, :entity.field1) are replaced in the generated code with corresponding Cassandra driver calls.
For example, for a String id parameter, something like statement.setString(1, id) will be generated, where the index corresponds to the parameter order in the query.
This ensures security (protection against CQL injection) and performance (using driver prepared statements).
Unlike relational databases, Cassandra has no transactions.
When you need several statements to be applied atomically, use a @Batch method (a CQL BATCH) as shown above;
its semantics and macros are documented in the common database section.
Profile¶
It is possible to override common settings with private settings from a profile, suppose there is such a profile configuration someProfile:
In order to apply the settings from the someProfile profile, just do the following:
The settings specified in the profile will be applied to each request, specifically in this case - a timeout of 10s will be set.
The profile applies only to the method annotated with @CassandraProfile; other repository methods continue to use the base configuration.
Mapping¶
It is possible to override the mapping of different parts of view and query parameters, Kora provides special interfaces for this.
Out of the box, CassandraModule provides mappers for common types: String, numeric types, Boolean, BigDecimal, BigInteger, UUID, ByteBuffer, LocalDate, LocalTime, LocalDateTime, ZonedDateTime, and Instant.
If a type is not covered by that set, or if it needs a custom representation in CQL, add a custom mapper through @Mapping.
View¶
Use the @EntityCassandra annotation for optimal view mapping.
The annotation allows the annotation processor to generate all necessary mappers in one round of annotation processing.
Without this annotation, mappers are generated on-demand, which can require multiple rounds of processing and significantly increase compilation time.
This is the recommended way to map every view returned from or bound into a repository.
All nested views and UDT types are also expected to use this annotation.
Result¶
If you need to convert the whole synchronous query result manually, use CassandraResultSetMapper<T>.
It receives ResultSet and returns the repository method value: a single object, list, Optional<T>, or another supported type.
final class ResultMapper implements CassandraResultSetMapper<List<UUID>> {
@Override
public List<UUID> apply(ResultSet rows) {
var result = new ArrayList<UUID>();
for (var row : rows) {
result.add(row.getUuid("id"));
}
return result;
}
}
@Repository
public interface EntityRepository extends CassandraRepository {
@Mapping(ResultMapper.class)
@Query("SELECT id FROM entities")
List<UUID> getIds();
}
In Kotlin, you only need to write mappers for T? types, so the type is specified as @Nullable in the interfaces.
class ResultMapper : CassandraResultSetMapper<List<UUID>> {
override fun apply(rows: ResultSet): List<UUID> {
return rows.map { it.getUuid("id") }
}
}
@Repository
interface EntityRepository : CassandraRepository {
@Mapping(ResultMapper::class)
@Query("SELECT id FROM entities")
fun getIds(): List<UUID>
}
Each result-mapper interface also exposes static factory helpers that build a full result mapper from a CassandraRowMapper<T>,
so you can reuse a single row mapper across signatures:
CassandraResultSetMapper—singleResultSetMapper,optionalResultSetMapper,listResultSetMapper;CassandraAsyncResultSetMapper—one,list(auto-paginates across result pages);CassandraReactiveResultSetMapper—flux,mono,monoVoid,monoList.
Row¶
If you need to convert one result row manually, use CassandraRowMapper<T>.
This mapper is applied to every row and suits return values like T, Optional<T>, List<T>, Flux<T>, and Flow<T>.
final class RowMapper implements CassandraRowMapper<UUID> {
@Override
public UUID apply(Row row) {
return UUID.fromString(row.getString(0));
}
}
@Repository
public interface EntityRepository extends CassandraRepository {
@Mapping(RowMapper.class)
@Query("SELECT id FROM entities")
List<UUID> findAll();
}
In Kotlin, you only need to write mappers for T? types, so the type is specified as @Nullable in the interfaces.
Column¶
If you need to convert the column value manually, it is suggested to use the CassandraRowColumnMapper:
public final class ColumnMapper implements CassandraRowColumnMapper<UUID> {
@Override
public UUID apply(GettableByName row, int index) {
return UUID.fromString(row.getString(index));
}
}
@Table("entities")
public record Entity(@Mapping(ColumnMapper.class) @Id UUID id, String name) { }
@Repository
public interface EntityRepository extends CassandraRepository {
@Query("SELECT id, name FROM entities")
List<Entity> findAll();
}
In Kotlin, you only need to write mappers for T? types, so the type is specified as @Nullable in the interfaces.
class ColumnMapper : CassandraRowColumnMapper<UUID> {
override fun apply(row: GettableByName, index: Int): UUID {
return UUID.fromString(row.getString(index))
}
}
@Table("entities")
data class Entity(
@Id @Mapping(ColumnMapper::class) val id: UUID,
val name: String
)
@Repository
interface EntityRepository : CassandraRepository {
@Query("SELECT id, name FROM entities")
fun findAll(): List<Entity>
}
Parameter¶
If you want to convert the value of a query parameter manually, use CassandraParameterColumnMapper<T>.
It receives SettableByName<?>, the parameter index, and the value from the repository method.
public final class ParameterMapper implements CassandraParameterColumnMapper<UUID> {
@Override
public void apply(SettableByName<?> stmt, int index, @Nullable UUID value) {
if (value != null) {
stmt.setString(index, value.toString());
}
}
}
@Repository
public interface EntityRepository extends CassandraRepository {
@Query("SELECT id, name FROM entities WHERE id = :id")
List<Entity> findById(@Mapping(ParameterMapper.class) UUID id);
}
In Kotlin, you only need to write mappers for T? types, so the type is specified as @Nullable in the interfaces.
class ParameterMapper : CassandraParameterColumnMapper<UUID?> {
override fun apply(stmt: SettableByName<*>, index: Int, value: UUID?) {
if (value != null) {
stmt.setString(index, value.toString())
}
}
}
@Repository
interface EntityRepository : CassandraRepository {
@Query("SELECT id, name FROM entities WHERE id = :id")
fun findById(@Mapping(ParameterMapper::class) id: UUID): List<Entity>
}
Async¶
For CompletionStage<T> and CompletableFuture<T>, use CassandraAsyncResultSetMapper<T>, which receives AsyncResultSet and returns CompletionStage<T>.
Its list helper automatically requests subsequent result pages, so a List<T> result gathers every page before completing.
For reactive types Mono<T> / Flux<T>, use CassandraReactiveResultSetMapper<T, P>, which receives ReactiveResultSet and returns the required Publisher.
final class ReactiveResultMapper implements CassandraReactiveResultSetMapper<UUID, Flux<UUID>> {
@Override
public Flux<UUID> apply(ReactiveResultSet rows) {
return Flux.from(rows).map(r -> UUID.fromString(r.getString(0)));
}
}
@Repository
public interface EntityRepository extends CassandraRepository {
@Mapping(ReactiveResultMapper.class)
@Query("SELECT id FROM entities")
Flux<UUID> getIds();
}
class ReactiveResultMapper : CassandraReactiveResultSetMapper<UUID, Flux<UUID>> {
override fun apply(rows: ReactiveResultSet): Flux<UUID> {
return Flux.from(rows).map { r -> UUID.fromString(r.getString(0)) }
}
}
@Repository
interface EntityRepository : CassandraRepository {
@Mapping(ReactiveResultMapper::class)
@Query("SELECT id FROM entities")
fun getIds(): Flux<UUID>
}
Manual Query¶
If a query is hard to express as a single static @Query, you can declare a regular method with an implementation and build CQL manually.
The repository exposes getCassandraConnectionFactory(), and CassandraConnectionFactory#query executes such a query:
it prepares the statement through the current CqlSession, wraps execution in Kora telemetry, and returns the value produced by the callback.
The currentSession() accessor returns the active CqlSession, and telemetry() returns the DataBaseTelemetry used for reporting.
QueryContext carries the query identifier and the final CQL.
The identifier is reported to telemetry, so use a stable name such as Repository.method.
Bind values through a BoundStatement obtained from the prepared statement; never concatenate values directly into the query string.
@Repository
public interface EntityRepository extends CassandraRepository {
default List<Entity> findByFilter(@Nullable String value2) {
var sql = new StringBuilder("SELECT id, value1, value2, value3 FROM entities");
if (value2 != null) {
sql.append(" WHERE value2 = ? ALLOW FILTERING");
}
var connectionFactory = getCassandraConnectionFactory();
var queryContext = new QueryContext("EntityRepository.findByFilter", sql.toString());
return connectionFactory.query(queryContext, statement -> {
var boundStatement = (value2 != null)
? statement.bind(value2)
: statement.bind();
var resultSet = connectionFactory.currentSession().execute(boundStatement);
var result = new ArrayList<Entity>();
for (var row : resultSet) {
result.add(new Entity(
row.getString("id"),
row.getInt("value1"),
row.getString("value2"),
row.getString("value3")));
}
return result;
});
}
}
@Repository
interface EntityRepository : CassandraRepository {
fun findByFilter(value2: String?): List<Entity> {
val sql = StringBuilder("SELECT id, value1, value2, value3 FROM entities")
if (value2 != null) {
sql.append(" WHERE value2 = ? ALLOW FILTERING")
}
val connectionFactory = cassandraConnectionFactory
val queryContext = QueryContext("EntityRepository.findByFilter", sql.toString())
return connectionFactory.query(queryContext) { statement ->
val boundStatement = if (value2 != null) statement.bind(value2) else statement.bind()
val resultSet = connectionFactory.currentSession().execute(boundStatement)
resultSet.map { row ->
Entity(
row.getString("id"),
row.getInt("value1"),
row.getString("value2"),
row.getString("value3")
)
}
}
}
}
Because Cassandra has no transactions, query simply runs on the current session with telemetry; there is no commit or rollback to manage.
UDT¶
There is support for UDT types through the @UDT annotation.
UDT describes a Cassandra user-defined type and can be used as a field of a regular entity.
The @UDT type is mapped like any other entity, so the enclosing entity is annotated with @EntityCassandra.
Given the following schema, where username is a user-defined type stored as a FROZEN column:
CREATE TYPE IF NOT EXISTS username(first text, last text);
CREATE TABLE IF NOT EXISTS entities_udt
(
id VARCHAR,
name FROZEN<username>,
PRIMARY KEY (id)
);
the view and repository look like this:
@Repository
public interface EntityRepository extends CassandraRepository {
@EntityCassandra
record Entity(String id, Name name) {
@UDT
record Name(String first, String last) {}
}
@Query("SELECT * FROM entities_udt WHERE id = :id")
@Nullable
Entity findById(String id);
@Query("""
INSERT INTO entities_udt(id, name)
VALUES (:entity.id, :entity.name)
""")
void insert(Entity entity);
}
@Repository
interface EntityRepository : CassandraRepository {
@EntityCassandra
data class Entity(val id: String, val name: Name) {
@UDT
data class Name(val first: String, val last: String)
}
@Query("SELECT * FROM entities_udt WHERE id = :id")
fun findById(id: String): Entity?
@Query("""
INSERT INTO entities_udt(id, name)
VALUES (:entity.id, :entity.name)
""")
fun insert(entity: Entity)
}
If the UDT type is not used through an enclosing entity, but as a standalone Cassandra type, mapper generation can be enabled explicitly with @EntityCassandra.
This is useful when the mapper is needed as a separate graph component.
Macros¶
To simplify writing CQL queries, use macros — they expand into CQL constructs at compile time.
Usage examples are shown above in the Usage section (findById and insert methods).
Detailed documentation: Common Database Rules — Macros
Signatures¶
Available signatures for repository methods out of the box:
T means the return value type, List<T>, or Void.
T myMethod()@Nullable T myMethod()Optional<T> myMethod()CompletionStage<T> myMethod()CompletionStageCompletableFuture<T> myMethod()CompletableFutureMono<T> myMethod()Project Reactor (require dependency)Flux<T> myMethod()Project Reactor (require dependency)
The CompletionStage<T>, CompletableFuture<T>, and Mono<T> wrappers can also wrap List<T>,
for example CompletionStage<List<Entity>> or Mono<List<Entity>>.
Method parameters can include regular values, DTOs, @Batch List<T> for batch execution, and CqlSession when the method needs access to the current driver session.
T means the return value type, T?, List<T>, or Unit.
myMethod(): Tsuspend myMethod(): TKotlin Coroutine (require dependency asimplementation)myMethod(): Flow<T>Kotlin Coroutine (require dependency asimplementation)
Method parameters can include regular values, DTOs, @Batch List<T> for batch execution, and CqlSession when the method needs access to the current driver session.
Telemetry¶
Logging, metrics, and tracing are configured via the telemetry block in the configuration and described in the Metrics Reference section.
To completely override telemetry, you can provide custom SPI factories; see the Common Database Documentation for details.