-
Notifications
You must be signed in to change notification settings - Fork 92
Table Mapping
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
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.
- 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
- 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
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
- 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
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
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
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;
...
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
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
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;
...
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
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
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 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
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.
To map a Java UUID
type to Cassandra timeuuid
type, you need to add the @TimeUUID
annotation on the target field.
To add a secondary index on a column, use the @Index
annotation. For more information on the usage, check @Index
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:
- have the same name as an existing mapped field
- have the same Java type as an existing mapped field (for primitive types, we rely on autoboxing so you can use
java.lang.Long
orlong
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;
}
}
Achilles does support immutability with the @Immutable
annotation. Put it on any of your entity.
The immutable entity should comply to the following rules
-
all fields should have
public final
modifiers - have neither getter nor setter
- 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
orlong
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 !!!!!!!!!!!!!!!
}
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
orpackage protected
visibility otherwise Achilles cannot access them and parse their annotations
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:
- globally using the attribute
namingStrategy()
on the@CompileTimeConfig
annotation: this strategy will apply to all entities - 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 |
-
Bootstraping Achilles at runtime
- Runtime Configuration Parameters
-
Manager
-
Consistency Level
-
Cassandra Options at runtime
-
Lightweight Transaction (LWT)
-
JSON Serialization
-
Interceptors
-
Bean Validation (JSR-303)