Skip to content

Table Mapping

DuyHai DOAN edited this page Aug 26, 2017 · 6 revisions

For bean mapping, only field-based access type is supported by Achilles. Furthermore, there is no default mapping for fields. If you want a field to be mapped, you must annotate it with @Column / @PartitionKey / @ClusteringColumn / @Computed. All fields that are not annotated by either annotations is considered transient by Achilles


Common rules

Achilles maps an entity into a Cassandra table. Each row represents an instance of the entity and each column represents a field.

For clustered entities, each tuple of (partition key, clustering column 1, ...clustering column N) represent a logical row although it translates into distinct physical columns by the underlying storage engine.

Entity matching

  • Achilles entities must conform to JavaBean convention with respect to accessors.
  • A Java bean is candidate to be an Achilles entity if it is annotated with @Table
  • All entities must have at least one field annotated with @PartitionKey
  • All entities must have a public constructor so that initialization block will be invoked when instantiated by Achilles

Table name

  • If the attribute table is provided by the @Table annotation, it will be used as table name in Cassandra for the entity
  • Otherwise, the table name is derived from the entity class name (not fully qualified name) by replacing all "." characters by "_"

In any case, the provided or derived table name should match the regexp pattern : [a-zA-Z0-9_]{1,48}

The limitation to 48 characters comes from Cassandra restriction on table name size

Examples:

	
	@Table
	public class MyEntity
	{
		@PartitionKey
		private Long id;
		...
	} 

Inferred table name = myentity

	@Table(table = "another_entity")
	public class AnotherEntity 
	{
		@PartitionKey
		private Long id;
		...
	} 

Inferred table name = another_entity


Field mapping

A field is managed by Achilles if it is annotated with @Column / @PartitionKey / @ClusteringColumn / @Computed. This is the explicit column mapping default behavior. You also can opt-in for an implicit column mapping behavior. See Column Mapping Strategy for more details on this


Column name
  • By default, the column name in Cassandra is the same as the entity field name.
  • The column name in Cassandra can be overridden by providing the value attribute on the @Column annotation.

On @PartitionKey / @ClusteringColumn fields, you can override the column name by adding a @Column annotation.

Examples:

	
	...
	@Column("tweet_id")
	@ClusteringColumn
	private UUID tweetId;
	...
	

Column name in Cassandra = name

	...
	@Column(name="age_in_year")
	private Integer age;

	...

Column name in Cassandra = age_in_year



Un-mapped fields

It is possible to have values stored in Cassandra that is not mapped to any field/property of the entity. In this case Achilles will simply ignore them



Column Mapping Strategy

The default strategy for field/column mapping is to use the @PartitionKey, @ClusteringColumn and @Column annotations. This is the explicit mapping strategy. However, you can choose an implicit mapping strategy so that Achilless will pick all fields of your Java bean as Cassandra columns, except those annotated by @Transient.

In any case, the @PartitionKey and @ClusteringColumn annotations are still required to define the primary key of your table.

You can specify the column mapping strategy at compile time using the @CompileTimeConfig annotation:

    @CompileTimeConfig(cassandraVersion = CassandraVersion.CASSANDRA_3_0_X, columnMappingStrategy = ColumnMappingStrategy.IMPLICIT)
    public interface AchillesConfig {
    
    }

The default behavior is ColumnMappingStrategy.EXPLICIT

See Configuring Achilles at compile time for more details



Static columns

Since Cassandra 2.0.6 it is possible to define static columns. A static column is a column which is related to the partition key(s). There is only one distinct value of static column per partition key.

Consequently it is only possible to define static column within an entity having at least one @ClsuteringColumn annotation

To define a static column, use the @Static annotation

  
  	...
  	
  	@Column
  	@Static
	private Integer age;
	...
	



Collection and Map support

With CQL, collection and maps are supported natively so there is nothing special to do for Achilles

Example:

	...
	@Column
	private List<String> addresses;
	...
	

In the CQL semantics, a null collection/map or an empty collection/map is equivalent. This is not the case in Java.

To avoid null check in client code, you can add the @EmptyCollectionIfNull annotation on the collection/map. This way when deserializing values from Cassandra, Achilles will instantiate an empty collection/map instead of null


Enum type

Let's consider the following enums

    public enum Country {  
        USA, CANADA, FRANCE, UNITED_KINGDOM, GERMANY ...
    }
        
    public enum Pricing {
        PREMIUM, VALUE, ECONOMIC ...
    }

Achilles does support enum types using the @Enumerated annotation. By default enums are serialized as String using the name() method.

Consequently, the ordering for enum types is based on their name, and not their natural ordering in Java (based on declaration order).

    @Column(name = "pricing")
    @Enumerated  // or @Enumerated(Encoding.NAME)
    private Pricing pricing;

This is an important detail and can be a gotcha if you don't pay attention when doing slice queries on entities having an enum as clustering column.

Achilles also lets you serialize the enums using their ordinal. In this case, you must change the value to Encoding.ORDINAL

    @Column(name = "pricing")
    @Enumerated(Encoding.ORDINAL)
    private Pricing pricing;

The @Enumerated annotation also works on collections & maps.

    @Column(name = "pricings")
    private List<@Enumerated(Encoding.NAME) Pricing> pricings;
    
    @Column(name = "pricing_per_country")
    private Map<@Enumerated(Encoding.ORDINAL) Country, @Enumerated(Encoding.NAME) PricingType> pricingPerCountry;    

Please note that when Achilles encounters an enum without the @Enumerated annotation (on simple value, collections or maps), it will encode it using the name. This is the default behavior


Counter type

Use the @Counter annotation on a column of type Long to indicate it is a counter column. Please note that if an entity contains a counter type, all other columns (normal and static) should also be of counter type. However, clustering columns can be of type different than counter.

Example:

    ...
    @Column
    @Counter
    private Long tweetsCount;
    ... 

UDT support

Achilles supports Cassandra UDT ( User Defined Type ) mapping. To mark a JavaBean as an UDT, just use the @UDT annotation:

    @UDT(keyspace = "my_ks", name="user_udt")
    public class UserUDT {
    
        @Column
        private String firstname;
        
        @Column
        private String lastname;
        
        ...
    }

Then you can re-use the UDT class in another entity

    @Table
    public class Tweet {
    
        @PartitionKey
        private UUID tweetId;
        
        @Column
        private String content;
        
        @Column
        @Frozen
        private UserUDT author;
    }

It is necessary to use the @Frozen annotation on the UDT field


Tuple types

Achilles also supports CQL tuples using the custom Tuple types.

    @Table
    public class Entity {
    
        ...
        
        @Column
        private Tuple3<Integer, String, List<String>> tuple3;
    
    }

There are 10 classes for tuple types: Tuple1, Tuple2, ..., Tuple10. See Tuples for more details

Note: it is not necessary to add @Frozen on tuple columns because they are by default already frozen. Nested collections, UDT or tuples do not need the @Frozen annotation either


Computed columns

It is possible to map the result of a function application to a column. For this, uses @Computed:

    @Table
    public class Entity {
    
        ...
        
        @Column
        private String value;
        
        @Computed(function = "writetime", alias="writetime_value", targetColumns = {"value"}, cqlClass = Long.class)
        private Long valueWriteTime;
    }

Please note that field mapped to computed values are only retrieve for SELECT query. They will be ignored for INSERT/UPDATE/DELETE.

For more details, see @Computed


Consistency Level

  • Consistency levels can be defined on the entity using the @Consistency annotation. This applies to all fields
  • Consistency levels can be overriden at runtime

Example:

    @Table(table = "user_tweets")
    @Consistency(read = QUORUM, write = QUORUM, serial = SERIAL)
    public class UseEntity {
    
        @PartitionKey
        private Long id;
    
        @Column
        private String name;
    
        ...
    }

In the above example:

  • the entity is read using QUORUM and inserted using QUORUM consistency levels
  • for LightWeight Transaction operations, Achilles will use the SERIAL consistency level

Value-less Entity

Achilles does support entity with only @PartitionKey / @ClusteringColumn fields. They are called value-less entities. A common use case for value-less entities is indexing data.


Time UUID

To map a Java UUID type to Cassandra timeuuid type, you need to add the @TimeUUID annotation on the target field.


Secondary Index

To add a secondary index on a column, use the @Index annotation. For more information on the usage, check @Index

Custom constructor

Sometime instead of the default no-arg constructor you want to define a custom constructor to be used by Achilles to create a new instance of your entity.

For this use case, please use the @EntityCreator annotation and put it on your custom constructor.

There should be maximum one non-default constructor annotated with @EntityCreator.

All the parameters of this custom constructor should:

  1. have the same name as an existing mapped field
  2. have the same Java type as an existing mapped field (for primitive types, we rely on autoboxing so you can use java.lang.Long or long for example, it does not matter)

Please note that it is not mandatory to inject all mapped-fields in your custom constructor, some fields can be set using setter or just let be null

If you wish to use different parameter name rather than sticking to the field name, you can declare manually all the field names matching the parameters using the value attribute

Example:

@Table
public class MyEntity {

    @PartitionKey
    private Long sensorId;

    @ClusteringColumn
    private Date date;

    @Column
    private Double value;

    //Correct custom constructor with matching field name and type
    @EntityCreator
    public MyEntity(Long sensorId, Date date, Double value) {
        this.sensorId = sensorId;
        this.date = date;
        this.value = value;
    }

    //Correct custom constructor with matching field name and type even if fewer parameters than existing field name
    @EntityCreator
    public MyEntity(Long sensorId, Date date) {
        this.sensorId = sensorId;
        this.date = date;
    }

    //Correct custom constructor with matching field name and autoboxed type (long)
    @EntityCreator
    public MyEntity(long sensorId, Date date, Double value) {
        this.sensorId = sensorId;
        this.date = date;
        this.value = value;
    }
    //Correct custom constructor with declared field name and type
     @EntityCreator({"sensorId", "date", "value"})
     public MyEntity(Long id, Date date, Double value) {
         this.sensorId = id;
         this.date = date;
         this.value = value;
     }

     //Incorrect custom constructor because non matching field name (myId)
     @EntityCreator
     public MyEntity(Long myId, Date date, Double value) {
         this.sensorId = myId;
         this.date = date;
         this.value = value;
     }

     //Incorrect custom constructor because field name not found (sensor_id)
     @EntityCreator({"sensor_id", "date", "value"})
     public MyEntity(Long sensor_id, Date date, Double value) {
         this.sensorId = sensor_id;
         this.date = date;
         this.value = value;
     }

     //Incorrect custom constructor because all field names are not declared (missing declaration of "value" in annotation)
     @EntityCreator({"sensorId", "date"})
     public MyEntity(Long sensor_id, Date date, Double value) {
         this.sensorId = sensor_id;
         this.date = date;
         this.value = value;
     }
}

Immutable Entity

Achilles does support immutability with the @Immutable annotation. Put it on any of your entity.

The immutable entity should comply to the following rules

  1. all fields should have public final modifiers
  2. have neither getter nor setter
  3. have exactly one non-default constructor that:
    • has as many argument as there are fields
    • each argument should have the same type as its corresponding field (for primitive types, we rely on autoboxing so you can use java.lang.Long or long for example, it does not matter)
    • each argument name should match an existing mapped field name

Example of correct mapping:

 @Table
 @Immutable
 public class MyImmutableEntity {
 
     @PartitionKey
     public final Long sensorId;
 
     @ClusteringColumn
     public final Date date;
 
     @Column
     public final Double value;
 
     //Correct non-default constructor with matching field name and type
     public MyImmutableEntity(long sensorId, Date date, Double value) {
         this.sensorId = sensorId;
         this.date = date;
         this.value = value;
     }
 
     // NO GETTER NOR SETTER !!!!!!!!!!!!!!!
 }

Example of wrong mapping because constructor argument name does not match field name:

@Table
@Immutable
public class MyImmutableEntity {

    @PartitionKey
    public final Long sensorId;

    @ClusteringColumn
    public final Date date;

    @Column
    public final Double value;

    //Incorrect, there is no field name "sensor_id" !!
    public MyImmutableEntity(long sensor_id, Date date, Double value) {
        this.sensorId = sensorId;
        this.date = date;
        this.value = value;
    }

    // NO GETTER NOR SETTER !!!!!!!!!!!!!!!
}

Example of wrong mapping because constructor argument type does not match field type:

 
@Table
@Immutable
public class MyImmutableEntity {

    @PartitionKey
    public final Long sensorId;

    @ClusteringColumn
    public final Date date;

    @Column
    public final Double value;

    //Incorrect, field sensorId is of type Long, not String !!
    public MyImmutableEntity(String sensor_id, Date date, Double value) {
        this.date = date;
        this.value = value;
    }

    // NO GETTER NOR SETTER !!!!!!!!!!!!!!!
}

Inheritance strategy

Achilles does support mapping inheritance. The strategy is one table per class. This is the most natural and straightforward strategy for inheritance.

Examples:

Case 1:

    @Table(table = "parent")
    public class ParentEntity {
      @PartitionKey 
      protected Long id;
    
      @Column
      protected String name;
    }

    @Table(table = "child")
    public class ChildEntity extends ParentEntity {
      @Column
      private String type;
    }
	

With the above example, Achilles will create the following tables:

    CREATE TABLE parent(
      id bigint PRIMARY KEY,
      name text
    );
    
    CREATE TABLE child(
      id bigint PRIMARY KEY,
      name text,
      type text
    );

Case 2:

	
    public class ParentEntity {
      @PartitionKey 
      private Long id;
    }
    
    @Table(table = "child1")
    public class Child1Entity extends ParentEntity {
      @Column
      private String name;
    }


    @Table(table = "child2")
    public class Child2Entity extends ParentEntity {
      @Column
      private String type;
    }
	

For this case, Achilles will create the following tables:

    CREATE TABLE child1(
      id bigint PRIMARY KEY,
      name text
    );
    
    CREATE TABLE child2(
      id bigint PRIMARY KEY,
      type text
    );

Please note that all fields in parent classes should have protected or package protected visibility otherwise Achilles cannot access them and parse their annotations


Naming Strategy

You can define a strategy to transform the keyspace name, table name and column name defined on your entities. Available strategies are:

  • SNAKE_CASE: transform all schema name using snake case
  • CASE_SENSITIVE: enclose the name between double quotes for escaping the case
  • LOWER_CASE: transform the name to lower case

The naming strategy can be defined at 2 places, by their ascending order of priority:

  1. globally using the attribute namingStrategy() on the @CompileTimeConfig annotation: this strategy will apply to all entities
  2. locally on each class using the @Strategy annotation

However, all those strategies are overriden if you set the name on the column manually using the name attribute of the @Column annotation

Naming Strategy priority

Priority (ascending order) Description
1 (lowest priority) Global naming strategy defined at compile time on @CompileTimeConfig
2 Locally on each entity using the @Strategy annotation
3 (highest priority) Defined using the value attribute on the @Column annotation

Home

Clone this wiki locally