blob: 55634bcefc6fed31c570665a72b63681b9e33105 [file] [log] [blame] [view]
## Entities
### Quick overview
POJO annotated with [@Entity], must expose a no-arg constructor.
* class-level annotations:
* [@NamingStrategy]
* [@CqlName]
* [@HierarchyScanStrategy]
* field/method-level annotations:
* [@PartitionKey], [@ClusteringColumn]
* [@Computed]
* [@Transient]
* [@CqlName]
* can inherit annotated fields/methods and [@NamingStrategy]. Only use [@Entity] on concrete
classes.
-----
An entity is a Java class that will be mapped to a Cassandra table or [UDT](../../core/udts).
Entities are used as arguments or return types of [DAO](../daos/) methods; they can also be nested
inside other entities (to map UDT columns).
In order to be detected by the mapper, the class must be annotated with [@Entity]:
```java
@Entity
public class Product {
@PartitionKey private UUID productId;
private String description;
public UUID getProductId() { return productId; }
public void setProductId(UUID productId) { this.productId = productId; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
}
```
Each entity property will be mapped to a CQL column. In order to detect a property:
* there **must** be a getter method that follows the usual naming convention (e.g. `getDescription`)
and has no parameters. The name of the property is obtained by removing the "get" prefix and
decapitalizing (`description`), and the type of the property is the return type of the getter.
* there **must** be a matching setter method (`setDescription`), with a single parameter that has
the same type as the property (the return type does not matter).
There *may* also be a matching field (`description`) that has the same type as the property, but
this is not mandatory: a property can have only a getter and a setter (for example if the value is
computed, or the field has a different name, or is nested into another field, etc.)
The class must expose a no-arg constructor that is at least package-private.
### Naming strategy
The mapper infers the database schema from your Java model: the entity class's name is converted
into a table name, and the property names into column names.
You can control the details of this conversion by annotating your entity class with
[@NamingStrategy].
#### Naming conventions
The simplest strategy is to use one of the mapper's built-in conventions:
```java
import static com.datastax.oss.driver.api.mapper.entity.naming.NamingConvention.UPPER_SNAKE_CASE;
@Entity
@NamingStrategy(convention = UPPER_SNAKE_CASE)
public class Product {
@PartitionKey private UUID productId;
...
}
```
Conventions convert names according to pre-defined rules. For example, with the `UPPER_SNAKE_CASE`
convention used above, the mapper expects the following schema:
```
CREATE TABLE "PRODUCT"("PRODUCT_ID" int primary key ...)
```
For the list of all available conventions, look at the enum constants in [NamingConvention].
If you don't annotate your class with [@NamingStrategy], the mapper defaults to the
`SNAKE_CASE_INSENSITIVE` convention.
#### User-provided name converter
If none of the built-in conventions work for you, you can provide your own conversion logic by
implementing [NameConverter]:
```java
public class MyNameConverter implements NameConverter {
@Override
public String toCassandraName(String javaName) {
... // implement your logic here
}
}
```
Then pass your converter class to the annotation:
```java
@Entity
@NamingStrategy(customConverterClass = MyNameConverter.class)
public class Product {
...
}
```
The mapper will use reflection to build an instance of the converter; it needs to expose a public
no-arg constructor.
Note that, unlike built-in conventions, the mapper processor cannot invoke your converter at compile
time and use the converted names directly in generated code. Instead, the generated code will invoke
the converter at runtime (that is, every time you run a query). If you want to squeeze the last bit
of performance from the mapper, we recommend sticking to conventions.
#### User-provided names
Finally, you can override the CQL name manually with the [@CqlName] annotation:
```java
@PartitionKey
@CqlName("id")
private UUID productId;
```
It works both on entity properties, and on the entity class itself.
This takes precedence over the entity-level naming strategy, so it's convenient if almost all of
your schema follows a convention, but you need exceptions for a few columns.
### Property annotations
Properties can be annotated to configure various aspects of the mapping. The annotation can be
either on the field, or on the getter (if both are specified, the mapper processor issues a
compile-time warning, and the field annotation will be ignored).
#### Primary key columns
If the entity maps to a table, properties that map to partition key columns must be annotated with
[@PartitionKey]:
```java
// CREATE TABLE sales(countryCode text, areaCode text, sales int,
// PRIMARY KEY((countryCode, areaCode)));
@PartitionKey(1)
private String countryCode;
@PartitionKey(2)
private String areaCode;
```
If the partition key is composite, the annotation's integer value indicates the position of each
property in the key. Note that any values can be used, but for clarity it's probably a good idea to
use consecutive integers starting at 0 or 1.
Similarly, properties that map to clustering columns must be annotated with [@ClusteringColumn]:
```java
// CREATE TABLE sensor_reading(id uuid, year int, month int, day int, value double,
// PRIMARY KEY(id, year, month, day));
@PartitionKey
private UUID id;
@ClusteringColumn(1)
private int year;
@ClusteringColumn(2)
private int month;
@ClusteringColumn(3)
private int day;
```
This information is used by some of the DAO method annotations; for example,
[@Select](../daos/select/)'s default behavior is to generate a selection by primary key.
#### Computed properties
Annotating an entity property with [@Computed] indicates that when retrieving data with the mapper
this property should be set to the result of a computation on the Cassandra side, typically a
function call:
```java
private int v;
@Computed("writetime(v)")
private long writetime;
```
The CQL return type of the formula must match the type of the property, otherwise an exception
will be thrown.
[@Computed] does not support case-sensitivity. If the expression contains case-sensitive column
or function names, you'll have to escape them:
```java
@Computed("\"myFunction\"(\"myColumn\")")
private int f;
```
[@Computed] fields are only used for select-based queries, so they will not be considered for
[@Update] or [@Insert] operations.
Also note that like all other properties, the expected name in a query result for a [@Computed]
property is based on the property name and the employed [@NamingStrategy](#naming-strategy). You may
override this behavior using [@CqlName](#user-provided-names).
Mapping computed results to property names is accomplished using [aliases]. If you wish to use
entities with [@Computed] properties with [@GetEntity] or [@Query]-annotated dao methods, you
must also do the same:
```java
@Entity
class MyEntity {
@PartitionKey private int k;
private int v;
@Computed("ttl(v)")
private int myTtl;
@Computed("writetime(v)")
@CqlName("ts")
private long writetime;
}
```
would expect a [@Query] such as:
```java
@Dao
class MyDao {
@Query("select k, v, ttl(v) as my_ttl, writetime(v) as ts from ${qualifiedTableId} where k=:id")
MyEntity findById(int id);
}
```
#### Transient properties
In some cases, one may opt to exclude properties defined on an entity from being considered
by the mapper. In this case, simply annotate these properties with [@Transient]:
```java
@Transient
private int notAColumn;
```
In addition, one may specify transient property names at the entity level by leveraging the
[@TransientProperties] annotation:
```java
@TransientProperties({"notAColumn", "x"})
@Entity
public class Product {
@PartitionKey private UUID id;
private String description;
// these columns are not included because their names are specified in @TransientProperties
private int notAColumn;
private int x;
}
```
Finally, any field including the `transient` keyword modifier will also be considered transient,
i.e.:
```java
private transient int notAColumn;
```
#### Custom column name
Override the CQL name manually with [@CqlName], see [User-provided names](#user-provided-names)
above.
### Default keyspace
You can specify a default keyspace to use when doing operations on a given entity:
```java
@Entity(defaultKeyspace = "inventory")
public class Product {
//....
}
```
This will be used when you build a DAO without an explicit keyspace parameter:
```java
@Mapper
public interface InventoryMapper {
@DaoFactory
ProductDao productDao();
@DaoFactory
ProductDao productDao(@DaoKeyspace String keyspace);
}
ProductDao productDao = mapper.productDao();
productDao.insert(product); // inserts into inventory.product
ProductDao productDaoTest = mapper.productDao("test");
productDaoTest.insert(product); // inserts into test.product
```
The default keyspace optional: if it is not specified, and you build a DAO without a keyspace, then
the session **must** have a default keyspace, otherwise an error will be thrown:
```java
@Entity
public class Product { ... }
CqlSession session = CqlSession.builder()
.withKeyspace("default_ks")
.build();
InventoryMapper mapper = new InventoryMapperBuilder(session).build();
ProductDao productDao = mapper.productDao();
productDao.insert(product); // inserts into default_ks.product
```
If you want the name to be case-sensitive, it must be enclosed in double-quotes, for example:
```java
@Entity(defaultKeyspace = "\"defaultKs\"")
```
### Inheritance
When mapping an entity class or a UDT class, the mapper will transparently scan superclasses and
parent interfaces for properties and annotations, thus enabling polymorphic mapping of one class
hierarchy into different CQL tables or UDTs.
Each concrete class must be annotated with [@Entity] and abstract classes and interfaces must not
use this annotation.
Here is an example of a polymorphic mapping:
```java
@Entity
static class Point2D {
private int x;
private int y;
@CqlName("\"X\"")
public int getX() { return x; }
public void setX(int x) { this.x = x; }
@CqlName("\"Y\"")
public int getY() { return y; }
public void setY(int y) { this.y = y; }
}
@Entity
static class Point3D extends Point2D {
private int z;
@CqlName("\"Z\"")
public int getZ() { return z; }
public void setZ(int z) { this.z = z; }
}
abstract static class Shape {
@PartitionKey // annotated field on superclass; annotation will get inherited in all subclasses
protected UUID id;
public abstract UUID getId();
public void setId(UUID id) { this.id = id; }
}
@CqlName("rectangles")
@Entity
static class Rectangle extends Shape {
private Point2D bottomLeft;
private Point2D topRight;
@CqlName("rect_id")
@Override
public UUID getId() { return id; }
public Point2D getBottomLeft() { return bottomLeft; }
public void setBottomLeft(Point2D bottomLeft) { this.bottomLeft = bottomLeft; }
public Point2D getTopRight() { return topRight; }
public void setTopRight(Point2D topRight) { this.topRight = topRight; }
public double getWidth() { return Math.abs(topRight.getX() - bottomLeft.getX()); }
public double getHeight() { return Math.abs(topRight.getY() - bottomLeft.getY()); }
}
@CqlName("circles")
@Entity
static class Circle extends Shape {
@CqlName("center2d")
protected Point2D center;
protected double radius;
@Override
@CqlName("circle_id")
public UUID getId() { return id; }
public double getRadius() { return this.radius; }
public Circle setRadius(double radius) {
this.radius = radius;
return this;
}
public Point2D getCenter() { return center; }
public void setCenter(Point2D center) { this.center = center; }
}
@CqlName("spheres")
@Entity
static class Sphere extends Circle {
@CqlName("sphere_id")
@Override
public UUID getId() { return id; }
// overrides field annotation in Circle,
// note that the property type is narrowed down to Point3D
@CqlName("center3d")
@Override
public Point3D getCenter() { return (Point3D) center; }
@Override
public void setCenter(Point2D center) {
assert center instanceof Point3D;
this.center = center;
}
// overridden builder-style setter
@Override
public Sphere setRadius(double radius) {
super.setRadius(radius);
return this;
}
}
```
The generated entity code should map to the following schema:
```
CREATE TYPE point2d ("X" int, "Y" int)
CREATE TYPE point3d ("X" int, "Y" int, "Z" int)
CREATE TABLE rectangles (rect_id uuid PRIMARY KEY, bottom_left frozen<point2d>, top_right frozen<point2d>)
CREATE TABLE circles (circle_id uuid PRIMARY KEY, center2d frozen<point2d>, radius double)
CREATE TABLE spheres (sphere_id uuid PRIMARY KEY, center3d frozen<point3d>, radius double)
```
Annotation priority is driven by proximity to the [@Entity] class. For example, in the code above
the use of `@CqlName("sphere_id")` on `Sphere.getId()` overrides the annotation
`@CqlName("circle_id")` on `Circle.getId()` for the `Sphere` entity.
Annotations declared on classes are given priority over annotations declared by interfaces
the same level.
To control how the class hierarchy is scanned, annotate classes with [@HierarchyScanStrategy].
[@ClusteringColumn]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/annotations/ClusteringColumn.html
[@CqlName]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/annotations/CqlName.html
[@Dao]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/annotations/Dao.html
[@Entity]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/annotations/Entity.html
[NameConverter]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/entity/naming/NameConverter.html
[NamingConvention]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/entity/naming/NamingConvention.html
[@NamingStrategy]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/annotations/NamingStrategy.html
[@PartitionKey]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/annotations/PartitionKey.html
[@Computed]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/annotations/Computed.html
[@Select]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/annotations/Select.html
[@Insert]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/annotations/Insert.html
[@Update]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/annotations/Update.html
[@GetEntity]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/annotations/GetEntity.html
[@Query]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/annotations/Query.html
[aliases]: http://cassandra.apache.org/doc/latest/cql/dml.html?#aliases
[@Transient]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/annotations/Transient.html
[@TransientProperties]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/annotations/TransientProperties.html
[@HierarchyScanStrategy]: https://docs.datastax.com/en/drivers/java/4.4/com/datastax/oss/driver/api/mapper/annotations/HierarchyScanStrategy.html