Spring Data for Apache Cassandra (original) (raw)

To take full advantage of the object mapping functionality inside the Spring Data for Apache Cassandra support, you should annotate your mapped domain objects with the @Table annotation. Doing so lets the classpath scanner find and pre-process your domain objects to extract the necessary metadata. Only annotated entities are used to perform schema actions. In the worst case, a SchemaAction.RECREATE_DROP_UNUSED operation drops your tables, and you lose your data. Note that tables are accessed from the session keyspace. You can however specify a custom keyspace to use a table/UDT from a specific keyspace.

The following example shows a simple domain object:

Example 4. Example domain object

package com.mycompany.domain;

@Table
public class Person {

  @Id
  private String id;

  @CassandraType(type = Name.VARINT)
  private Integer ssn;

  private String firstName;

  private String lastName;
}

| | The @Id annotation tells the mapper which property you want to use for the Cassandra primary key. Composite primary keys can require a slightly different data model. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |

Working with Primary Keys

Cassandra requires at least one partition key field for a CQL table. A table can additionally declare one or more clustering key fields. When your CQL table has a composite primary key, you must create a @PrimaryKeyClass to define the structure of the composite primary key. In this context, “composite primary key” means one or more partition columns optionally combined with one or more clustering columns.

Primary keys can make use of any singular simple Cassandra type or mapped user-defined Type. Collection-typed primary keys are not supported.

Simple Primary Keys

A simple primary key consists of one partition key field within an entity class. Since it is one field only, we safely can assume it is a partition key. The following listing shows a CQL table defined in Cassandra with a primary key of user_id:

Example 5. CQL Table defined in Cassandra

CREATE TABLE user (
  user_id text,
  firstname text,
  lastname text,
  PRIMARY KEY (user_id))
;

The following example shows a Java class annotated such that it corresponds to the Cassandra defined in the previous listing:

Example 6. Annotated Entity

@Table(value = "login_event")
public class LoginEvent {

  @PrimaryKey("user_id")
  private String userId;

  private String firstname;
  private String lastname;

  // getters and setters omitted

}

Composite Keys

Composite primary keys (or compound keys) consist of more than one primary key field. That said, a composite primary key can consist of multiple partition keys, a partition key and a clustering key, or a multitude of primary key fields.

Composite keys can be represented in two ways with Spring Data for Apache Cassandra:

The simplest form of a composite key is a key with one partition key and one clustering key.

The following example shows a CQL statement to represent the table and its composite key:

Example 7. CQL Table with a Composite Primary Key

CREATE TABLE login_event(
  person_id text,
  event_code int,
  event_time timestamp,
  ip_address text,
  PRIMARY KEY (person_id, event_code, event_time))
  WITH CLUSTERING ORDER BY (event_time DESC)
;

Flat Composite Primary Keys

Flat composite primary keys are embedded inside the entity as flat fields. Primary key fields are annotated with@PrimaryKeyColumn. Selection requires either a query to contain predicates for the individual fields or the use of MapId. The following example shows a class with a flat composite primary key:

Example 8. Using a flat composite primary key

@Table(value = "login_event")
class LoginEvent {

  @PrimaryKeyColumn(name = "person_id", ordinal = 0, type = PrimaryKeyType.PARTITIONED)
  private String personId;

  @PrimaryKeyColumn(name = "event_code", ordinal = 1, type = PrimaryKeyType.PARTITIONED)
  private int eventCode;

  @PrimaryKeyColumn(name = "event_time", ordinal = 2, type = PrimaryKeyType.CLUSTERED, ordering = Ordering.DESCENDING)
  private LocalDateTime eventTime;

  @Column("ip_address")
  private String ipAddress;

  // getters and setters omitted
}

Primary Key Class

A primary key class is a composite primary key class that is mapped to multiple fields or properties of the entity. It is annotated with @PrimaryKeyClass and should define equals and hashCode methods. The semantics of value equality for these methods should be consistent with the database equality for the database types to which the key is mapped. Primary key classes can be used with repositories (as the Id type) and to represent an entity’s identity in a single complex object. The following example shows a composite primary key class:

Example 9. Composite primary key class

@PrimaryKeyClass
class LoginEventKey implements Serializable {

  @PrimaryKeyColumn(name = "person_id", ordinal = 0, type = PrimaryKeyType.PARTITIONED)
  private String personId;

  @PrimaryKeyColumn(name = "event_code", ordinal = 1, type = PrimaryKeyType.PARTITIONED)
  private int eventCode;

  @PrimaryKeyColumn(name = "event_time", ordinal = 2, type = PrimaryKeyType.CLUSTERED, ordering = Ordering.DESCENDING)
  private LocalDateTime eventTime;

  // other methods omitted
}

The following example shows how to use a composite primary key:

Example 10. Using a composite primary key

@Table(value = "login_event")
public class LoginEvent {

  @PrimaryKey
  private LoginEventKey key;

  @Column("ip_address")
  private String ipAddress;

  // getters and setters omitted
}

Embedded Entity Support

Embedded entities are used to design value objects in your Java domain model whose properties are flattened out into the table. In the following example you see, that User.name is annotated with @Embedded. The consequence of this is that all properties of UserName are folded into the user table which consists of 3 columns (user_id, firstname, lastname).

| | Embedded entities may only contain simple property types. It is not possible to nest an embedded entity into another embedded one. | | ------------------------------------------------------------------------------------------------------------------------------------- |

However, if the firstname and lastname column values are actually null within the result set, the entire property name will be set to null according to the onEmpty of @Embedded, which nulls objects when all nested properties are null.
Opposite to this behavior USE_EMPTY tries to create a new instance using either a default constructor or one that accepts nullable parameter values from the result set.

Example 11. Sample Code of embedding objects

public class User {

    @PrimaryKey("user_id")
    private String userId;

    @Embedded(onEmpty = USE_NULL) (1)
    UserName name;
}

public class UserName {
    private String firstname;
    private String lastname;
}
1 Property is null if firstname and lastname are null. Use onEmpty=USE_EMPTY to instantiate UserName with a potential null value for its properties.

You can embed a value object multiple times in an entity by using the optional prefix element of the @Embedded annotation. This element represents a prefix and is prepended to each column name in the embedded object. Note that properties will overwrite each other if multiple properties render to the same column name.

| | Make use of the shortcuts @Embedded.Nullable and @Embedded.Empty for @Embedded(onEmpty = USE_NULL) and @Embedded(onEmpty = USE_EMPTY) to reduce verbosity and simultaneously set JSR-305 @javax.annotation.Nonnull accordingly. public class MyEntity { @Id Integer id; @Embedded.Nullable (1) EmbeddedEntity embeddedEntity; } 1 Shortcut for @Embedded(onEmpty = USE_NULL). | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

Mapping Annotation Overview

The MappingCassandraConverter can use metadata to drive the mapping of objects to rows in a Cassandra table. An overview of the annotations follows:

The mapping metadata infrastructure is defined in the separate, spring-data-commons project that is both technology- and data store-agnostic.

The following example shows a more complex mapping:

Example 12. Mapped Person class

@Table("my_person")
public class Person {

    @PrimaryKeyClass
    public static class Key implements Serializable {

        @PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED)
        private String type;

        @PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.PARTITIONED)
        private String value;

        @PrimaryKeyColumn(name = "correlated_type", ordinal = 2, type = PrimaryKeyType.CLUSTERED)
        private String correlatedType;

        // other getters/setters omitted
    }

    @PrimaryKey
    private Person.Key key;

    @CassandraType(type = CassandraType.Name.VARINT)
    private Integer ssn;

    @Column("f_name")
    private String firstName;

    @Column
    @Indexed
    private String lastName;

    private Address address;

    @CassandraType(type = CassandraType.Name.UDT, userTypeName = "myusertype")
    private UdtValue usertype;

    private Coordinates coordinates;

    @Transient
    private Integer accountTotal;

    @CassandraType(type = CassandraType.Name.SET, typeArguments = CassandraType.Name.BIGINT)
    private Set<Long> timestamps;

    private Map<@Indexed String, InetAddress> sessions;

    public Person(Integer ssn) {
        this.ssn = ssn;
    }

    public Person.Key getKey() {
        return key;
    }

    // no setter for Id.  (getter is only exposed for some unit testing)

    public Integer getSsn() {
        return ssn;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    // other getters/setters omitted
}

The following example shows how to map a UDT Address:

Example 13. Mapped User-Defined Type Address

@UserDefinedType("address")
public class Address {

  @CassandraType(type = CassandraType.Name.VARCHAR)
  private String street;

  private String city;

  private Set<String> zipcodes;

  @CassandraType(type = CassandraType.Name.SET, typeArguments = CassandraType.Name.BIGINT)
  private List<Long> timestamps;

  // other getters/setters omitted
}

| | Working with User-Defined Types requires a UserTypeResolver that is configured with the mapping context. See the configuration chapter for how to configure a UserTypeResolver. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |

The following example shows how map a tuple:

Example 14. Mapped Tuple

@Tuple
class Coordinates {

  @Element(0)
  @CassandraType(type = CassandraType.Name.VARCHAR)
  private String description;

  @Element(1)
  private long longitude;

  @Element(2)
  private long latitude;

  // other getters/setters omitted
}

Index Creation

You can annotate particular entity properties with @Indexed, @SaiIndexed, or @SASI if you wish to create secondary indexes on application startup. Index creation creates simple secondary indexes for scalar types, user-defined types, and collection types.

You can configure a SASI Index to apply an analyzer, such as StandardAnalyzer or NonTokenizingAnalyzer (by using@StandardAnalyzed and @NonTokenizingAnalyzed, respectively).

Map types distinguish between ENTRY, KEYS, and VALUES indexes. Index creation derives the index type from the annotated element. The following example shows a number of ways to create an index:

Example 15. Variants of map indexing

@Table
class PersonWithIndexes {

  @Id
  private String key;

  @SASI
  @StandardAnalyzed
  private String names;

  @SaiIndexed
  @VectorType(dimensions = 1536) // required for table generation
  private Vector vector;

  @Indexed("indexed_map")
  private Map<String, String> entries;

  private Map<@Indexed String, String> keys;

  private Map<String, @Indexed String> values;

  // …
}

| | The @Indexed annotation can be applied to single properties of embedded entities or along with the @Embedded annotation, in which case all properties of the embedded are indexed. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

| | Index creation on session initialization may have a severe performance impact on application startup. | | -------------------------------------------------------------------------------------------------------- |