DATACASS-272 - Add mapping/converter configuration and repository support to documentation.
This commit is contained in:
@@ -101,17 +101,123 @@ class ApplicationConfig extends AbstractCassandraConfiguration {
|
||||
As our domain repository extends `CrudRepository` it provides you with CRUD operations.
|
||||
Working with the repository instance is just a matter of dependency injecting it into a client.
|
||||
|
||||
.Paging access to Person entities
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration
|
||||
public class PersonRepositoryTests {
|
||||
|
||||
[[cassandradb.repositories.queries]]
|
||||
@Autowired PersonRepository repository;
|
||||
|
||||
@Test
|
||||
public void readsPersonTableCorrectly() {
|
||||
|
||||
List<Person> persons = repository.findAll();
|
||||
assertThat(persons.isEmpty(), is(false));
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The sample creates an application context with Spring's unit test support which will perform annotation based dependency injection into test cases. Inside the test method we simply use the repository to query the datastore. We invoke the repository query method that requests the all `Person` instances.
|
||||
|
||||
[[cassandra.repositories.queries]]
|
||||
== Query methods
|
||||
|
||||
[[cassandradb.repositories.queries.delete]]
|
||||
=== Repository delete queries
|
||||
Most of the data access operations you usually trigger on a repository result a query being executed against the Cassandra database. Defining such a query is just a matter of declaring a method on the repository interface.
|
||||
|
||||
[[cassandradb.repositories.misc]]
|
||||
.PersonRepository with query methods
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
public interface PersonRepository extends CrudRepository<Person, String> {
|
||||
|
||||
List<Person> findByLastname(String lastname); <1>
|
||||
|
||||
List<Person> findByFirstname(String firstname, Sort sort); <2>
|
||||
|
||||
Person findByShippingAddresses(Address address); <3>
|
||||
|
||||
Stream<Person> findAllBy(); <4>
|
||||
}
|
||||
----
|
||||
<1> The method shows a query for all people with the given lastname. The query will be derived parsing the method name for constraints which can be concatenated with `And`. Thus the method name will result in a query expression of `SELECT * from person WHERE lastname = 'lastname'`.
|
||||
<2> Applies dynamic sorting to a query. Just equip your method signature with a `Sort` parameter and we will automatically apply sortin to the query accordingly.
|
||||
<3> Shows that you can query based on properties which are not a primitive type using registered `Converter` 's in `CustomConversions`.
|
||||
<4> Uses a Java 8 `Stream` which reads and converts individual elements while iterating the stream.
|
||||
====
|
||||
|
||||
NOTE: Note that querying non-primary key properties requires secondary indexes.
|
||||
|
||||
[cols="1,2,3", options="header"]
|
||||
.Supported keywords for query methods
|
||||
|===
|
||||
| Keyword
|
||||
| Sample
|
||||
| Logical result
|
||||
|
||||
| `After`
|
||||
| `findByBirthdateAfter(Date date)`
|
||||
| `birthdate > date`
|
||||
|
||||
| `GreaterThan`
|
||||
| `findByAgeGreaterThan(int age)`
|
||||
| `age > age`
|
||||
|
||||
| `GreaterThanEqual`
|
||||
| `findByAgeGreaterThanEqual(int age)`
|
||||
| `age >= age`
|
||||
|
||||
| `Before`
|
||||
| `findByBirthdateBefore(Date date)`
|
||||
| `birthdate < date`
|
||||
|
||||
| `LessThan`
|
||||
| `findByAgeLessThan(int age)`
|
||||
| `age < age`
|
||||
|
||||
| `LessThanEqual`
|
||||
| `findByAgeLessThanEqual(int age)`
|
||||
| `age <= age`
|
||||
|
||||
| `In`
|
||||
| `findByAgeIn(Collection ages)`
|
||||
| `age IN (ages...)`
|
||||
|
||||
| `Like`, `StartingWith`, `EndingWith`
|
||||
| `findByFirstnameLike(String name)`
|
||||
| `firstname LIKE (name as like expression)`
|
||||
|
||||
| `Containing` on String
|
||||
| `findByFirstnameContaining(String name)`
|
||||
| `firstname LIKE (name as like expression)`
|
||||
|
||||
| `Containing` on Collection
|
||||
| `findByAddressesContaining(Address address)`
|
||||
| `addresses CONTAINING address`
|
||||
|
||||
| `(No keyword)`
|
||||
| `findByFirstname(String name)`
|
||||
| `firstname = name`
|
||||
|
||||
| `IsTrue`, `True`
|
||||
| `findByActiveIsTrue()`
|
||||
| `active = true`
|
||||
|
||||
| `IsFalse`, `False`
|
||||
| `findByActiveIsFalse()`
|
||||
| `active = false`
|
||||
|
||||
|===
|
||||
|
||||
include::../{spring-data-commons-docs}/repository-projections.adoc[leveloffset=+2]
|
||||
|
||||
[[cassandra.repositories.misc]]
|
||||
== Miscellaneous
|
||||
|
||||
[[cassandradb.repositories.misc.cdi-integration]]
|
||||
[[cassandra.repositories.misc.cdi-integration]]
|
||||
=== CDI Integration
|
||||
|
||||
Instances of the repository interfaces are usually created by a container, which Spring is the most natural choice when working with Spring Data. Spring Data Cassandra ships with a custom CDI extension that allows using the repository abstraction in CDI environments. The extension is part of the JAR so all you need to do to activate it is dropping the Spring Data Cassandra JAR into your classpath. You can now set up the infrastructure by implementing a CDI Producer for the `CassandraTemplate`:
|
||||
|
||||
@@ -825,22 +825,96 @@ NOTE: For more information on the Spring type conversion service see the referen
|
||||
[[cassandra.custom-converters.writer]]
|
||||
=== Saving using a registered Spring Converter
|
||||
|
||||
Coming Soon!
|
||||
An example implementation of the `Converter` that converts a `Person` object to a `java.lang.String` using Jackson 2 is shown below:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
|
||||
static class PersonWriteConverter implements Converter<Person, String> {
|
||||
|
||||
public String convert(Person source) {
|
||||
|
||||
try {
|
||||
return new ObjectMapper().writeValueAsString(source);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[cassandra.custom-converters.reader]]
|
||||
=== Reading using a Spring Converter
|
||||
|
||||
Coming Soon!
|
||||
An example implementation of the `Converter` that converts a `java.lang.String` into a `Person` object using Jackson 2 is shown below:
|
||||
|
||||
[[cassandra.custom-converters.xml]]
|
||||
[source,java]
|
||||
----
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.codehaus.jackson.map.ObjectMapper;
|
||||
|
||||
static class PersonReadConverter implements Converter<String, Person> {
|
||||
|
||||
public Person convert(String source) {
|
||||
|
||||
if (StringUtils.hasText(source)) {
|
||||
try {
|
||||
return new ObjectMapper().readValue(source, Person.class);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[cassandra.custom-converters.java]]
|
||||
=== Registering Spring Converters with the CassandraConverter
|
||||
|
||||
Coming Soon!
|
||||
The Spring Data Cassandra Java Config provides a convenient way to register Spring `Converter` s with the `MappingCassandraConverter`. The configuration snippet below shows how to manually register converters as well as configuring the `CustomConversions`.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
public static class Config extends AbstractCassandraConfiguration {
|
||||
|
||||
@Override
|
||||
public CustomConversions customConversions() {
|
||||
|
||||
List<Converter<?, ?>> converters = new ArrayList<Converter<?, ?>>();
|
||||
converters.add(new PersonReadConverter());
|
||||
converters.add(new PersonWriteConverter());
|
||||
|
||||
return new CustomConversions(converters);
|
||||
}
|
||||
|
||||
// other methods omitted...
|
||||
}
|
||||
----
|
||||
|
||||
[[cassandra.converter-disambiguation]]
|
||||
=== Converter disambiguation
|
||||
|
||||
Coming Soon!
|
||||
Generally we inspect the `Converter` implementations for the source and target types they convert from and to. Depending on whether one of those is a type Cassandra can handle natively we will register the converter instance as reading or writing one. Have a look at the following samples:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
// Write converter as only the target type is one cassandra can handle natively
|
||||
class MyConverter implements Converter<Person, String> { … }
|
||||
|
||||
// Read converter as only the source type is one cassandra can handle natively
|
||||
class MyConverter implements Converter<String, Person> { … }
|
||||
----
|
||||
|
||||
In case you write a `Converter` whose source and target type are native cassandra types there's no way for us to determine whether we should consider it as reading or writing converter. Registering the converter instance as both might lead to unwanted results then. E.g. a `Converter<String, Long>` is ambiguous although it probably does not make sense to try to convert all `String` instances into `Long` instances when writing. To be generally able to force the infrastructure to register a converter for one way only we provide `@ReadingConverter` as well as `@WritingConverter` to be used at the converter implementation.
|
||||
|
||||
[[cassandra-template.commands]]
|
||||
== Executing Commands
|
||||
|
||||
@@ -10,7 +10,7 @@ In this section we will describe the features of the CassandraMappingConverter.
|
||||
|
||||
`CassandraMappingConverter` has a few conventions for mapping objects to CQL Tables when no additional mapping metadata is provided. The conventions are:
|
||||
|
||||
* The short Java class name is mapped to the table name in the following manner. The class `com.bigbank.SavingsAccount` maps to `savings_account` table name.
|
||||
* The short Java class name is mapped to the table name in the following manner. The class `com.bigbank.SavingsAccount` maps to `savingsaccount` table name.
|
||||
* The converter will use any Spring Converters registered with it to override the default mapping of object properties to document field/values.
|
||||
* The properties of an object are used to convert to and from properties in the document.
|
||||
|
||||
@@ -88,6 +88,20 @@ In addition to these types, Spring Data Cassandra provides a set of built-in con
|
||||
| `Enum`
|
||||
| `text` (default), `bigint`, `varint`, `int`, `smallint`, `tinyint`
|
||||
|
||||
| `LocalDate` +
|
||||
(Joda, Java 8, JSR310-BackPort)
|
||||
| `date`
|
||||
|
||||
| `LocalDateTime`, `LocalTime`, `Instant` +
|
||||
(Joda, Java 8, JSR310-BackPort)
|
||||
| `timestamp`
|
||||
|
||||
| `DateMidnight` (Joda)
|
||||
| `date`
|
||||
|
||||
| `ZoneId` (Java 8, JSR310-BackPort)
|
||||
| `text`
|
||||
|
||||
|===
|
||||
|
||||
Each supported type maps to a default
|
||||
@@ -120,18 +134,153 @@ NOTE: `Enum` mapping using ordinal values requires at least Spring 4.3.0. Using
|
||||
|
||||
Unless explicitly configured, an instance of `CassandraMappingConverter` is created by default when creating a `CassandraTemplate` . You can create your own instance of the `MappingCassandraConverter` so as to tell it where to scan the classpath at startup your domain classes in order to extract metadata and construct indexes. Also, by creating your own instance you can register Spring converters to use for mapping specific classes to and from the database.
|
||||
|
||||
You can configure the `CassandraMappingConverter` and CassandraTemplate either using Java or XML based metadata. Here is an example using Spring's Java based configuration
|
||||
|
||||
.@Configuration class to configure Cassandra mapping support
|
||||
====
|
||||
TODO
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
public static class Config extends AbstractCassandraConfiguration {
|
||||
|
||||
@Override
|
||||
protected String getKeyspaceName() {
|
||||
return "bigbank";
|
||||
}
|
||||
|
||||
// the following are optional
|
||||
|
||||
@Override
|
||||
public CustomConversions customConversions() {
|
||||
|
||||
List<Converter<?, ?>> converters = new ArrayList<Converter<?, ?>>();
|
||||
converters.add(new PersonReadConverter());
|
||||
converters.add(new PersonWriteConverter());
|
||||
|
||||
return new CustomConversions(converters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SchemaAction getSchemaAction() {
|
||||
return SchemaAction.RECREATE;
|
||||
}
|
||||
|
||||
// other methods omitted...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
.XML schema to configure Cassandra mapping support
|
||||
`AbstractCassandraConfiguration` requires you to implement methods that define a keyspace. `AbstractCassandraConfiguration` also has a method you can override named `getEntityBasePackages(…)` which tells the converter where to scan for classes annotated with the `@Table` annotation.
|
||||
|
||||
You can add additional converters to the converter by overriding the method `customConversions`.
|
||||
|
||||
NOTE: `AbstractCassandraConfiguration` will create a `CassandraTemplate` instance and registered with the container under the name `cassandraTemplate`.
|
||||
|
||||
|
||||
[[mapping-usage]]
|
||||
== Metadata based Mapping
|
||||
|
||||
To take full advantage of the object mapping functionality inside the Spring Data/Cassandra support, you should annotate your mapped objects with the `@Table` annotation. It allows the classpath scanner to find and pre-process your domain objects to extract the necessary metadata. If you don't use this annotation your entities will be not found or rejected, if used in repository definitions or during runtime. Only annotated entities will be used to perform schema actions. In the worst case a `SchemaAction.RECREATE_DROP_UNUSED` will drop your tables and you'll experience data loss.
|
||||
|
||||
.Example domain object
|
||||
====
|
||||
TODO
|
||||
[source,java]
|
||||
----
|
||||
package com.mycompany.domain;
|
||||
|
||||
@Table
|
||||
public class Person {
|
||||
|
||||
@Id
|
||||
private ObjectId id;
|
||||
|
||||
@CassandraType(type = Name.VARINT)
|
||||
private Integer ssn;
|
||||
|
||||
private String firstName;
|
||||
|
||||
private String lastName;
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
IMPORTANT: 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.
|
||||
|
||||
|
||||
[[mapping-usage-annotations]]
|
||||
=== Mapping annotation overview
|
||||
|
||||
The `MappingCassandraConverter` can use metadata to drive the mapping of objects to rows. An overview of the annotations is provided below
|
||||
|
||||
* `@Id` - applied at the field or property level to mark the property used for identity purpose.
|
||||
* `@Table` - applied at the class level to indicate this class is a candidate for mapping to the database. You can specify the name of the table where the database will be stored.
|
||||
* `@PrimaryKey` - Similar to `@Id` but allows to specify the column name
|
||||
* `@PrimaryKeyColumn` - Cassandra-specific annotation for primary key columns that allows to specify primary key column attributes such as for clustered/partitioned. Can be used on single and multiple attributes to indicate either a single or a compound primary key.
|
||||
* `@PrimaryKeyClass` - applied at the class level to indicate this class is a compound primary key class. Requires to be references with `@PrimaryKey`
|
||||
* `@Transient` - by default all private fields are mapped to the row, this annotation excludes the field where it is applied from being stored in the database
|
||||
* `@Column` - applied at the field level. Describes the name of the column as it will be represented in the Cassandra table thus allowing the name to be different than the fieldname of the class.
|
||||
* `@CassandraType` - applied at the field level to specify a Cassandra data type. Types are derived from the declaration by default.
|
||||
|
||||
The mapping metadata infrastructure is defined in a separate spring-data-commons project that is technology agnostic.
|
||||
|
||||
Here is an example of a more complex mapping.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@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 ommitted
|
||||
}
|
||||
|
||||
@PrimaryKey
|
||||
private Person.Key key;
|
||||
|
||||
@CassandraType(type = Name.VARINT)
|
||||
private Integer ssn;
|
||||
|
||||
@Column("f_name")
|
||||
private String firstName;
|
||||
|
||||
@Column(forceQuote = true)
|
||||
private String lastName;
|
||||
|
||||
@Transient
|
||||
private Integer accountTotal;
|
||||
|
||||
@CassandraType(type = Name.SET, typeArguments = Name.BIGINT)
|
||||
private Set<Long> timestamps;
|
||||
|
||||
private Map<String, InetAddress> sessions;
|
||||
|
||||
public Person(Integer ssn) {
|
||||
this.ssn = ssn;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
// no setter for Id. (getter is only exposed for some unit testing)
|
||||
|
||||
public Integer getSsn() {
|
||||
return ssn;
|
||||
}
|
||||
|
||||
// other getters/setters ommitted
|
||||
----
|
||||
|
||||
[[mapping-explicit-converters]]
|
||||
=== Overriding Mapping with explicit Converters
|
||||
|
||||
|
||||
Reference in New Issue
Block a user