Apache Cassandra Java Driver

repository·4.x·Indexed 23 days ago

https://github.com/apache/cassandra-java-driver

A modern, feature-rich, and highly tunable Java client library for Apache Cassandra, DataStax Enterprise, and DataStax Astra. It exclusively uses Cassandra's binary protocol and CQL v3. Key features include an Object Mapper for mapping rows to POJOs, a Query Builder and Schema Builder for programmatic CQL construction, native metrics bindings for Micrometer and MicroProfile, and support for reactive programming, vector data types, and asynchronous paging.

Tokens
129.7K
Snippets
323
Records
576
Agent score
80%

What's inside apache-cassandra-java-driver

  1. Understand the driver module structure

    4.x

    The Apache Cassandra Java Driver is organized into several specialized modules depending on your use case:

    • Core: The primary entry point for the driver. Use this for managing connectivity to Cassandra clusters and executing CQL queries.
    • Query builder: Provides a fluent API for programmatically constructing CQL queries without manual string concatenation.
    • Mapper: An object-mapping layer that handles the boilerplate of executing queries and transforming database rows into application-level Java objects.
    • Developer docs: Contains information regarding the internal codebase and extension points for advanced customization and plugin development.
  2. Understand the driver architecture for advanced customizations

    4.x

    The developer documentation provides a deep dive into the driver's internal layers. This material is intended for driver contributors and framework architects building advanced customizations or integrations. To understand how the driver functions from the ground up, it is recommended to follow this progression of layers:

    1. Common Infrastructure: The foundational components used across the driver.
    2. Native Protocol Layer: Handles the binary encoding of TCP payloads.
    3. Netty Pipeline: Manages networking and low-level stream management.
    4. Request Execution: Handles high-level processing of user requests and responses.
    5. Administrative Tasks: Manages cluster state and metadata.

    Note that much of this material involves internal packages. Refer to the API conventions for guidance on how to interact with these components safely.

  3. What is a control connection and how is it used

    4.x

    The control connection is a dedicated connection used by the driver for administrative tasks, separate from the regular pooled connections used for data queries. Its primary responsibilities include:

    • Querying system tables to discover cluster topology and schema.
    • Checking for schema agreement across the cluster.
    • Reacting to server events to notify the driver of external topology or schema changes.

    Because it is managed independently, the control connection is additive to your regular connection pools. For example, if you configure a pool size of 2, Node.getOpenConnections or the pool.open-connections metric will report 3 connections (2 for the pool + 1 for control).

  4. Overview of Driver Administrative Components

    4.x

    The driver uses several specialized components to track cluster state and metadata, working alongside the main request execution path. These components include:

    • Control Connection: Maintains a dedicated DriverChannel to listen for server-side protocol events (topology and schema) and query system tables.
    • Metadata Manager: Maintains the immutable Metadata object accessible via session.getMetadata().
    • Topology Monitor: Abstracts how node information is retrieved and how topology changes (node added/removed) or status changes (node up/down) are detected.
    • Node State Manager: Tracks the actual state of nodes by combining external signals (like gossip) with observed internal state (active connections).
    • Event Bus: The central communication hub where components publish and subscribe to events like topology changes, schema changes, and node state changes.
  5. Combine @StatementAttributes and function parameters

    4.x

    You can use both @StatementAttributes and a function parameter in the same DAO method. When both are present, the mapper applies the annotation first, and then applies the function. This allows the function to override or augment the static attributes defined by the annotation.

    @Dao
    public interface ProductDao {
      @Select
      @StatementAttributes(consistencyLevel = "ONE", pageSize = 500)
      Product findById(
          int productId, Function<BoundStatementBuilder, BoundStatementBuilder> setAttributes);
    }
    
    // If the function sets consistency to QUORUM, the final statement 
    // will use CL = QUORUM and page size = 500
    Product product =
        dao.findById(1, builder -> builder.setConsistencyLevel(DefaultConsistencyLevel.QUORUM));
  6. Understand the distinction between Public and Internal APIs

    4.x

    Starting with version 4.0, the driver uses package naming to separate the official public API from internal implementation details. This helps manage the API surface and provides hooks for advanced customization.

    Public API

    • Package: com.datastax.oss.driver.api
    • Purpose: Intended for regular client applications to execute queries.
    • Compatibility: Follows semantic versioning; binary compatibility is guaranteed across minor and patch versions.

    Internal API

    • Package: com.datastax.oss.driver.internal
    • Purpose: Primarily for internal communication between driver components, and secondarily for advanced customization.
    • Compatibility: Backward compatibility is "best-effort" only and not formally guaranteed.
    • Risk: Using internal APIs is more complex and carries the potential to break the driver. It is recommended to have familiarity with the source code before using these components.
  7. How User-Defined Types (UDTs) work

    4.x

    A User-Defined Type (UDT) is an ordered set of named, typed fields (e.g., { street: '1 Main St', zip: 12345 }).

    In Cassandra, UDTs must be defined within a keyspace. They can be used as column types in tables or as field types within other UDTs. The driver handles these via the UdtValue class for reading/writing data and the UserDefinedType class for representing the schema definition.

    Warning on Manual Type Construction: The driver's official public API does not provide a way to manually build UserDefinedType instances. This is to prevent users from creating types that do not precisely match the database schema (e.g., incorrect field order), which can lead to data corruption. Manually constructed types are considered "detached."

  8. Performance considerations for traversal serialization

    4.x

    Before sending a fluent graph statement over the network, the driver serializes the Gremlin traversal into a byte array. This serialization happens on the client thread, even when using asynchronous modes.

    • For explicit execution, serialization occurs on the thread calling session.execute or session.executeAsync.
    • For implicit execution, serialization occurs on the thread calling the terminal step.

    If a single thread issues many session.executeAsync calls in a tight loop, serialization can become a CPU bottleneck. To resolve this, profile your application to confirm CPU saturation on the client thread and distribute session.executeAsync calls across more threads.

  9. How the Metadata Manager works

    4.x

    The MetadataManager is responsible for maintaining the contents of session.getMetadata().

    Key Characteristics:

    • Immutability: The Metadata object is immutable and updated atomically, ensuring a consistent view of the cluster (e.g., a keyspace in a token map will always have corresponding KeyspaceMetadata).
    • Concurrency: It uses a 'confined inner class' pattern to ensure all metadata refreshes are applied serially by a single admin thread, preventing race conditions.
    • Refresh Mechanism: Transitions are managed by MetadataRefresh objects. The manager performs full schema refreshes (rather than incremental) using SchemaQueries (to fetch data) and SchemaParser (to transform data).

    Data Sources:

    • For node-related data, it queries the TopologyMonitor.
    • For schema-related data, it uses the ControlConnection directly.
  10. How encoding and decoding works via FrameCodec

    4.x

    Encoding and decoding are handled by a FrameCodec, which manages the transformation between frames and binary data.

    Key Components

    • Message.Codec: Every message has a corresponding codec for encoding and decoding. A FrameCodec uses a set of these codecs based on the protocol version and opcode.
    • CodecGroup: A convenience utility to register multiple codecs at once. The driver uses default implementations like ProtocolV3ClientCodecs, ProtocolV4ClientCodecs, etc.
    • Compressor: An optional component used to compress frame bodies.
    • PrimitiveCodec<B>: An abstraction that defines how to interact with the underlying binary container B (e.g., reading/writing integers). This allows the protocol layer to be agnostic of the specific binary type (the driver uses Netty's ByteBuf, but it could be swapped for byte[]).

    Initializing a FrameCodec

    To initialize a FrameCodec, you need:

    1. A PrimitiveCodec.
    2. An optional Compressor.
    3. One or more CodecGroups.
    public interface PrimitiveCodec<B> {
      B allocate(int size);
      int readInt(B source);
      void writeInt(int i, B dest);
      ...
    }
  11. Perform primary key selections with @Select

    4.x

    When using @Select without a custom WHERE clause, you can select by the full primary key or a subset of it, provided you satisfy certain rules:

    1. Full Primary Key: Provide all partition key and clustering column components in the correct order.
    2. Subset of Primary Key: You can specify a subset of clustering columns to select multiple entities within a partition, but you must provide all partition key components.
    3. Clustering Column Order: All preceding clustering columns in the primary key definition must be provided if any are. For example, if your key is (partition_key, clustering_col_1, clustering_col_2), you cannot skip clustering_col_1 to provide clustering_col_2.
    4. No Parameters: Providing no parameters will select all rows in the table.

    Important: If your method takes a partial primary key, the first parameter that is not a primary key component must be explicitly annotated with @CqlName to prevent the mapper from confusing it with a primary key component.