CODE WITH SIBIN

Solving Real Problems with Real Code


Spring Data Cassandra Annotations Guide with Examples

This comprehensive guide covers all major Spring Data Cassandra annotations with practical examples and detailed explanations.

1. Entity Mapping Annotations

@Table - Mapping a Class to a Table

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

@Table("users") // Maps to "users" table in Cassandra
public class User {
    // fields...
}

Explanation:

  • Marks a class as a persistent entity mapped to a Cassandra table
  • If no name is specified, the simple class name is used (case-sensitive)
  • Best practice is to explicitly specify the table name

@PrimaryKey and @PrimaryKeyClass - Composite Primary Keys

import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.PrimaryKeyClass;
import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn;
import org.springframework.data.cassandra.core.cql.PrimaryKeyType;

// Entity class
@Table("orders")
public class Order {
    @PrimaryKey 
    private OrderKey key;
    
    private Double total;
    private String status;
    // getters/setters...
}

// Composite primary key class
@PrimaryKeyClass
public class OrderKey implements Serializable {
    @PrimaryKeyColumn(name = "customer_id", ordinal = 0, type = PrimaryKeyType.PARTITIONED)
    private String customerId;
    
    @PrimaryKeyColumn(name = "order_id", ordinal = 1, type = PrimaryKeyType.CLUSTERED)
    private UUID orderId;
    // getters/setters, equals(), hashCode()...
}

Explanation:

  • @PrimaryKey marks a field as the primary key (used with composite keys)
  • @PrimaryKeyClass marks a class as a composite primary key
  • The key class must implement Serializable
  • @PrimaryKeyColumn specifies column details:
    • name: column name in table
    • ordinal: ordering of columns in composite key
    • typePARTITIONED for partition key, CLUSTERED for clustering column

@Column - Field to Column Mapping

@Table("products")
public class Product {
    @PrimaryKey
    private UUID id;
    
    @Column("product_name") // Maps to "product_name" column
    private String name;
    
    @Column // If omitted, defaults to field name ("price")
    private Double price;
    
    @Transient // Won't be persisted
    private String temporaryNote;
}

Explanation:

  • Explicitly maps a field to a table column
  • Optional if field name matches column name (case-sensitive)
  • Can specify custom column name

@CassandraType - Explicit Type Mapping

import com.datastax.oss.driver.api.core.type.DataType;
import com.datastax.oss.driver.api.core.type.DataTypes;

@Table("sensor_data")
public class SensorData {
    @PrimaryKey
    private UUID sensorId;
    
    @CassandraType(type = DataType.Name.TIMESTAMP)
    private Instant readingTime;
    
    @CassandraType(type = DataType.Name.DECIMAL)
    private BigDecimal value;
    
    @CassandraType(type = DataType.Name.LIST, typeArguments = DataType.Name.DOUBLE)
    private List<Double> measurements;
}

Explanation:

  • Provides explicit type information when it can't be inferred
  • Useful for complex types like collections, UDTs, or custom mappings
  • typeArguments specifies generic type parameters for collections

@Transient - Excluding Fields

@Table("employees")
public class Employee {
    @PrimaryKey
    private UUID id;
    
    private String name;
    private String department;
    
    @Transient
    private String fullName; // Won't be persisted
    
    // Calculated property
    public String getFullName() {
        return name + " (" + department + ")";
    }
}

Explanation:

  • Marks a field as non-persistent
  • Useful for calculated properties or temporary fields
  • Similar to transient in Java serialization

2. Query and Repository Annotations

@Query - Custom CQL Queries

import org.springframework.data.cassandra.repository.Query;
import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<User, String> {
    
    @Query("SELECT * FROM users WHERE last_name = ?0 ALLOW FILTERING")
    List<User> findByLastName(String lastName);
    
    @Query("UPDATE users SET status = ?1 WHERE user_id = ?0")
    @AllowFiltering
    void updateUserStatus(String userId, String status);
}

Explanation:

  • Defines custom CQL queries for repository methods
  • Parameters are bound by position (?0?1, etc.)
  • Can be used for both read and write operations
  • Supports most CQL features

@AllowFiltering - Enabling Filtering

public interface ProductRepository extends CrudRepository<Product, UUID> {
    
    @AllowFiltering
    List<Product> findByCategory(String category);
    
    @Query("SELECT * FROM products WHERE price > ?0")
    @AllowFiltering
    List<Product> findExpensiveProducts(double minPrice);
}

Explanation:

  • Enables filtering for queries that would otherwise require a full table scan
  • Use with caution as it can impact performance significantly
  • Better to design proper tables with appropriate primary keys

3. Key Structure Annotations

@PartitionKey and @ClusteringColumn

@Table("time_series_data")
public class TimeSeriesData {
    @PrimaryKey
    private TimeSeriesKey key;
    
    private Double value;
    // getters/setters...
}

@PrimaryKeyClass
public class TimeSeriesKey implements Serializable {
    @PartitionKey // Partition key component
    private String sensorType;
    
    @ClusteringColumn // Clustering column (defines sort order)
    private Instant timestamp;
    
    @ClusteringColumn(1) // Second clustering column
    private UUID sensorId;
    // getters/setters, equals(), hashCode()...
}

Explanation:

  • @PartitionKey marks fields that form the partition key
  • @ClusteringColumn marks fields that form clustering columns
  • The ordinal value determines the order of clustering columns
  • Proper key design is crucial for Cassandra performance

4. Miscellaneous Annotations

@PersistenceConstructor - Constructor Selection

@Table("books")
public class Book {
    @PrimaryKey
    private UUID id;
    private String title;
    private String author;
    private Integer pages;
    
    // Default constructor
    public Book() {}
    
    // Preferred constructor for persistence
    @PersistenceConstructor
    public Book(UUID id, String title, String author) {
        this.id = id;
        this.title = title;
        this.author = author;
    }
    
    // Additional constructors...
}

Explanation:

  • Specifies which constructor to use when instantiating entities from database results
  • Useful when multiple constructors exist
  • Parameters should match column names or be annotated with @Param

@Indexed - Secondary Indexes

@Table("customers")
public class Customer {
    @PrimaryKey
    private UUID id;
    
    @Indexed // Creates secondary index
    private String email;
    
    private String name;
    // getters/setters...
}

Explanation:

  • Marks a column to be indexed (secondary index)
  • Note: In Cassandra, secondary indexes have limitations and performance implications
  • Often better to denormalize data into query-specific tables

Complete Example Application

Let's put it all together in a complete example:

1. Domain Model

// Composite key for messages
@PrimaryKeyClass
public class MessageKey implements Serializable {
    @PartitionKey
    private String recipient;
    
    @ClusteringColumn
    private UUID messageId;
    
    @ClusteringColumn(1)
    private Instant sentTime;
    // constructors, getters/setters, equals(), hashCode()...
}

// Message entity
@Table("messages")
public class Message {
    @PrimaryKey
    private MessageKey key;
    
    private String sender;
    private String subject;
    
    @Column("body_text")
    private String text;
    
    @CassandraType(type = DataType.Name.LIST, typeArguments = DataType.Name.TEXT)
    private List<String> attachments;
    
    @Transient
    private String preview; // Not persisted
    
    @PersistenceConstructor
    public Message(MessageKey key, String sender, String subject, String text, List<String> attachments) {
        this.key = key;
        this.sender = sender;
        this.subject = subject;
        this.text = text;
        this.attachments = attachments != null ? attachments : new ArrayList<>();
    }
    
    // Accessors...
    public String getPreview() {
        return text != null && text.length() > 50 ? text.substring(0, 50) + "..." : text;
    }
}

2. Repository Interface

public interface MessageRepository extends CrudRepository<Message, MessageKey> {
    
    // Find all messages for a recipient
    List<Message> findByKeyRecipient(String recipient);
    
    // Find recent messages for a recipient
    @Query("SELECT * FROM messages WHERE recipient = ?0 AND sentTime > ?1")
    List<Message> findRecentMessages(String recipient, Instant since);
    
    // Search in subject (with secondary index)
    @Indexed
    List<Message> findBySubjectContaining(String term);
    
    // Count messages from a sender
    @Query("SELECT COUNT(*) FROM messages WHERE sender = ?0 ALLOW FILTERING")
    long countBySender(String sender);
}

3. Service Layer Example

@Service
public class MessageService {
    private final MessageRepository repository;
    
    public MessageService(MessageRepository repository) {
        this.repository = repository;
    }
    
    public Message sendMessage(String sender, String recipient, String subject, String text) {
        MessageKey key = new MessageKey(recipient, UUID.randomUUID(), Instant.now());
        Message message = new Message(key, sender, subject, text, null);
        return repository.save(message);
    }
    
    public List<Message> getInbox(String recipient) {
        return repository.findByKeyRecipient(recipient);
    }
    
    public List<Message> searchInbox(String recipient, String term) {
        List<Message> allMessages = getInbox(recipient);
        return allMessages.stream()
            .filter(m -> m.getText().contains(term) || m.getSubject().contains(term))
            .collect(Collectors.toList());
    }
}

Best Practices

  1. Key Design:
    • Design your primary keys based on query patterns
    • Partition keys should distribute data evenly
    • Clustering columns define sort order within partitions
  2. Annotations Usage:
    • Prefer @PrimaryKeyColumn for simple keys
    • Use @PrimaryKeyClass for composite keys
    • Be explicit with @Column names for clarity
  3. Performance Considerations:
    • Avoid @AllowFiltering in production when possible
    • Use secondary indexes (@Indexed) sparingly
    • Consider denormalization for query performance
  4. Type Safety:
    • Use @CassandraType for complex types
    • Consider custom converters for special type mappings
  5. Immutable Objects:
    • Consider making entities immutable with @PersistenceConstructor
    • This aligns well with Cassandra's append-only nature

This guide covers all essential Spring Data Cassandra annotations with practical examples. The framework provides powerful abstractions while still allowing access to Cassandra's unique features.

Leave a Reply

Your email address will not be published. Required fields are marked *