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