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

Example 1. Coherence Spring Data Dependencies
Maven
<dependency>
    <groupId>com.oracle.coherence.spring</groupId>
    <artifactId>coherence-spring-data</artifactId>
    <version>3.0.0</version>
</dependency>
Gradle
implementation("com.oracle.coherence.spring:coherence-spring-data:3.0.0")

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:

A projection interface to retrieve a subset of attributes
interface BookTitleAndPages {

    String getTitle();
    int getPages();
}
A repository using an interface based projection with a query method
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:

A projection interface to retrieve a subset of attributes
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:

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:

An open projection
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:

A projection interface using a default method for custom logic
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:

A projection interface using a default method for custom logic
@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:

A sample Book object
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

A projection interface using nullable wrappers
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:

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:

A repository using a dynamic projection parameter
@CoherenceMap("book")
interface BookRepository extends CrudRepository<Book, UUID> {

    <T> Collection<T> findByTitle(String title, Class<T> type);
}
Using a repository with dynamic projections
Collection<BookSummary> aggregates = books.findByTitle("Shadow", BookSummary.class);

Collection<BookTitleAndPages> aggregates = books.findByTitle("Remember", BookTitleAndPages.class);