1. Introduction
The Spring Data Coherence module provides integration with Coherence data grids. Key functional areas of Spring Data Coherence are a POJO centric model for interacting with a Coherence data grid and easily writing a Repository style data access layer.
2. Features
-
Spring configuration support using Java-based @Configuration classes.
-
Automatic implementation of Repository interfaces
-
Rich query and event features from Coherence
-
Native asynchronous repository support
-
Projections
3. Getting Started
<dependency>
<groupId>com.oracle.coherence.spring</groupId>
<artifactId>coherence-spring-data</artifactId>
<version>3.0.0-SNAPSHOT</version>
</dependency>
implementation("com.oracle.coherence.spring:coherence-spring-data:3.0.0-SNAPSHOT")
4. Defining Repositories
Before proceeding, please be familiar with the Spring Data Commons documentation, as this section will assume some familiarity with Spring Data.
Simple repositories such as the following will, of course, work as expected:
public interface PersonRepository extends CrudRepository<String, Person> {
// ...
}
However, it is recommended to extend the CoherenceRepository interface, to fully utilize the features Coherence for Spring Data has to offer such as:
-
Powerful projection features
-
Flexible in-place entity updates
-
First-class data aggregation support
-
Stream API support
-
Event listener support
-
Declarative acceleration and index creation
-
Native asynchronous support
Please also see the Coherence Repository documentation for more details on these features.
Example extending the blocking CoherenceRepository
interface:
import com.oracle.coherence.spring.data.repository.CoherenceRespository;
// ...
public interface PersonRepository extends CoherenceRepository<String, Person> {
// ...
}
or for the non-blocking flavor:
import com.oracle.coherence.spring.data.repository.CoherenceAsyncRespository;
// ...
public interface PersonRepository extends CoherenceAsyncRepository<String, Person> {
// ...
}
4.1. Identifying the Coherence NamedMap
The Coherence NamedMap
that will be used by the Repository implementation
will be based on the type name in the Repository class assuming the Repository
name follows the format of [Type]Repository (e.g., PersonRepository will use a NamedMap called person
).
If this is not desired, the name may instead be passed by the @CoherenceMap
annotation. For example:
import com.oracle.coherence.spring.data.config.CoherenceMap;
import com.oracle.coherence.spring.data.repository.CoherenceRespository;
// ...
@CoherenceMap("people")
public interface PersonRepository extends CoherenceRepository<String, Person> {
// ...
}
5. Mapping Entities
As Coherence is, at its core, a key-value store, mapping Entities for use with a Coherence
Repository is relatively simple as only the id needs to be annotated. It is possible to
use either org.springframework.data.annotation.Id
or javax.persistence.Id
to denote the
entity’s id.
For example:
public class Person implements Serializable {
@org.springframework.data.annotation.Id
protected String id;
// ---- person functionality ----
}
6. Using the Repository
In order to enable Coherence-based Repositories, you must use the @EnableCoherenceRepositories annotation. A simple configuration example would be:
import com.oracle.coherence.spring.configuration.annotation.EnableCoherence;
import com.oracle.coherence.spring.configuration.data.config.EnableCoherenceRepositories;
// ...
@Configuration
@EnableCoherence
@EnableCoherenceRepositories
public static class Config {
}
Similarly to other Spring Data implementations, the @EnableCoherenceRepositories
annotation
offers several configuration options to configure how Spring will search for repositories.
Please see the API docs for details.
6.1. Finder Queries
One of the benefits of Spring Data is the ability to define queries on the Repository interface using Spring Data’s finder query syntax. For example:
import com.oracle.coherence.spring.data.repository.CoherenceRespository;
import com.oracle.coherence.spring.data.model.Author;
import com.oracle.coherence.spring.data.model.Book;
import com.tangosol.util.UUID;
// ...
public interface BookRepository extends CoherenceRepository<Book, UUID> {
List<Book> findByAuthor(Author author);
// other finders
}
It should be noted that finder queries defined on either the blocking or non-blocking Coherence repository will always execute in a blocking manner. For asynchronous versions of these methods, please use Spring’s Async Method support.
import com.oracle.coherence.spring.data.config.CoherenceMap;
import com.oracle.coherence.spring.data.model.Author;
import com.oracle.coherence.spring.data.model.Book;
import com.oracle.coherence.spring.data.repository.CoherenceAsyncRepository;
import com.tangosol.util.UUID;
import org.springframework.scheduling.annotation.Async;
// ...
@CoherenceMap("book")
public interface CoherenceBookAsyncRepository extends CoherenceAsyncRepository<Book, UUID> {
@Async
CompletableFuture<List<Book>> findByAuthor(Author author);
}
Don’t forget to include the @EnableAsync
annotation on the configuration:
import com.oracle.coherence.spring.configuration.annotation.EnableCoherence;
import com.oracle.coherence.spring.configuration.data.config.EnableCoherenceRepositories;
import org.springframework.scheduling.annotation.EnableAsync;
// ...
@Configuration
@EnableAsync
@EnableCoherence
@EnableCoherenceRepositories
public static class Config {
}
7. Projections
Spring Data Coherence module supports projections as defined in Spring Data Projections documentation. This allows us,
among other things, to transfer a subset of the entities properties when closed
projections are used.
Imagine a repository and aggregate root type such as the following example:
@Entity
public class Book implements Cloneable, Serializable {
@Id
protected final UUID uuid;
protected String title;
protected Author author;
protected int pages;
protected Calendar published;
}
@CoherenceMap("book")
interface BookRepository extends CrudRepository<Book, UUID> {
List<BookProjection> findByTitle(String title);
// ...
}
7.1. Interface-based Projections
The simplest way to simplify the result is to declare an interface that exposes methods reading the desired properties, as shown in the following example:
interface BookTitleAndPages {
String getTitle();
int getPages();
}
interface BookRepository extends CrudRepository<Book, UUID> {
List<BookTitleAndPages> findByTitle(String title);
}
The query execution engine creates proxy instances of that interface at runtime for each element returned and forwards calls to the exposed methods to the target object.
Projections can be used recursively as shown in the following example:
interface BookSummary {
String getTitle();
int getPages();
AuthorSummary getAuthor();
interface AuthorSummary {
String getFirstName();
}
}
7.2. Closed Projections
A projection interface whose accessor methods all match properties of the target aggregate is considered to be a closed projection. The following example is a closed projection:
interface BookTitleAndPages {
String getTitle();
int getPages();
}
7.3. Open Projections
Accessor methods in projection interfaces can also be used to compute new values by using the @Value annotation, as shown in the following example:
interface BookTitleAndAuthor {
@Value("#{target.author.firstName + ' - ' + target.title}")
String getTitleAndAuthor();
}
A projection interface using @Value
is an open projection. Spring Data cannot apply query execution optimizations in this case, because the SpEL expression could use any attribute of the aggregate root.
Default methods also can be used for open projection interfaces:
interface BookTitleAndAuthor {
default String getTitleAndAuthor() {
return getAuthor().getFirstName().concat(' - ').concat(getTitle());
}
}
A more flexible option is to implement the custom logic in a Spring bean:
@Component
class MyBean {
String getTitleAndAuthor(Book book) {
return book.getAuthor().getFirstName().concat(' - ').concat(book.getTitle());
}
}
interface BookTitleAndAuthor {
@Value("#{@myBean.getTitleAndAuthor(target)}")
String getTitleAndAuthor();
}
Methods backed by SpEL expression evaluation can also use method parameters, which can then be referred to from the expression. The method parameters are available through an Object array named args. The following example shows how to get a method parameter from the args array:
interface NameOnly {
@Value("args[0] + ' ' + #{target.author.firstName}")
String getHonorificName(String honorific);
}
7.4. Nullable Wrappers
Getters in projection interfaces can make use of nullable wrappers for improved null-safety. Currently supported wrapper types are:
-
java.util.Optional
-
com.google.common.base.Optional
-
scala.Option
-
io.vavr.control.Option
interface TitleOnly {
Optional<String> getTitle();
}
7.5. Class-based Projections (DTOs)
Another way of defining projections is by using value type DTOs (Data Transfer Objects) that hold properties for the fields that are supposed to be retrieved - similar to the projection interfaces except that no proxying happens and no nested projections can be applied.
The fields to be loaded are determined from the parameter names of the constructor that is exposed.
The following example shows a projecting DTO:
class BookTitleAndPages {
private final String title;
private final int pages;
BookTitleAndPages(String title, int pages) {
this.title = title;
this.pages = pages;
}
String getTitle() {
return this.title;
}
int getPages() {
return this.pages;
}
// equals(…) and hashCode() implementations
}
7.6. Dynamic Projections
It’s possible to select the return type to be used at invocation time (up to now all examples shown exact projection type to be used as a return type). To apply dynamic projections, use a query method such as the one shown in the following example:
@CoherenceMap("book")
interface BookRepository extends CrudRepository<Book, UUID> {
<T> Collection<T> findByTitle(String title, Class<T> type);
}
Collection<BookSummary> aggregates = books.findByTitle("Shadow", BookSummary.class);
Collection<BookTitleAndPages> aggregates = books.findByTitle("Remember", BookTitleAndPages.class);