Skip to content

Latest commit

 

History

History
381 lines (291 loc) · 14.2 KB

r2dbc-repositories.adoc

File metadata and controls

381 lines (291 loc) · 14.2 KB

R2DBC Repositories

This chapter points out the specialties for repository support for R2DBC. This chapter builds on the core repository support explained in [repositories]. Before reading this chapter, you should have a sound understanding of the basic concepts explained there.

Usage

To access domain entities stored in a relational database, you can use our sophisticated repository support that eases implementation quite significantly. To do so, create an interface for your repository. Consider the following Person class:

Example 1. Sample Person entity
public class Person {

  @Id
  private Long id;
  private String firstname;
  private String lastname;

  // … getters and setters omitted
}

The following example shows a repository interface for the preceding Person class:

Example 2. Basic repository interface to persist Person entities
public interface PersonRepository extends ReactiveCrudRepository<Person, Long> {

  // additional custom query methods go here
}

To configure R2DBC repositories, you can use the @EnableR2dbcRepositories annotation. If no base package is configured, the infrastructure scans the package of the annotated configuration class. The following example shows how to use Java configuration for a repository:

Example 3. Java configuration for repositories
@Configuration
@EnableR2dbcRepositories
class ApplicationConfig extends AbstractR2dbcConfiguration {

  @Override
  public ConnectionFactory connectionFactory() {
    return …;
  }
}

Because our domain repository extends ReactiveCrudRepository, it provides you with reactive CRUD operations to access the entities. On top of ReactiveCrudRepository, there is also ReactiveSortingRepository, which adds additional sorting functionality similar to that of PagingAndSortingRepository. Working with the repository instance is merely a matter of dependency injecting it into a client. Consequently, you can retrieve all Person objects with the following code:

Example 4. Paging access to Person entities
@RunWith(SpringRunner.class)
@ContextConfiguration
public class PersonRepositoryTests {

  @Autowired PersonRepository repository;

  @Test
  public void readsAllEntitiesCorrectly() {

    repository.findAll()
      .as(StepVerifier::create)
      .expectNextCount(1)
      .verifyComplete();
  }

  @Test
  public void readsEntitiesByNameCorrectly() {

    repository.findByFirstname("Hello World")
      .as(StepVerifier::create)
      .expectNextCount(1)
      .verifyComplete();
  }
}

The preceding example creates an application context with Spring’s unit test support, which performs annotation-based dependency injection into test cases. Inside the test method, we use the repository to query the database. We use StepVerifier as a test aid to verify our expectations against the results.

Query Methods

Most of the data access operations you usually trigger on a repository result in a query being run against the databases. Defining such a query is a matter of declaring a method on the repository interface, as the following example shows:

Example 5. PersonRepository with query methods
interface ReactivePersonRepository extends ReactiveSortingRepository<Person, Long> {

  Flux<Person> findByFirstname(String firstname);                                   (1)

  Flux<Person> findByFirstname(Publisher<String> firstname);                        (2)

  Flux<Person> findByFirstnameOrderByLastname(String firstname, Pageable pageable); (3)

  Mono<Person> findByFirstnameAndLastname(String firstname, String lastname);       (4)

  Mono<Person> findFirstByLastname(String lastname);                                (5)

  @Query("SELECT * FROM person WHERE lastname = :lastname")
  Flux<Person> findByLastname(String lastname);                                     (6)

  @Query("SELECT firstname, lastname FROM person WHERE lastname = $1")
  Mono<Person> findFirstByLastname(String lastname);                                (7)
}
  1. The method shows a query for all people with the given lastname. The query is derived by parsing the method name for constraints that can be concatenated with And and Or. Thus, the method name results in a query expression of SELECT … FROM person WHERE firstname = :firstname.

  2. The method shows a query for all people with the given firstname once the firstname is emitted by the given Publisher.

  3. Use Pageable to pass offset and sorting parameters to the database.

  4. Find a single entity for the given criteria. It completes with IncorrectResultSizeDataAccessException on non-unique results.

  5. Unless <4>, the first entity is always emitted even if the query yields more result documents.

  6. The findByLastname method shows a query for all people with the given last name.

  7. A query for a single Person entity projecting only firstname and lastname columns. The annotated query uses native bind markers, which are Postgres bind markers in this example.

The following table shows the keywords that are supported for query methods:

Table 1. 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

Between

findByAgeBetween(int from, int to)

age BETWEEN from AND to

NotBetween

findByAgeBetween(int from, int to)

age NOT BETWEEN from AND to

In

findByAgeIn(Collection<Integer> ages)

age IN (age1, age2, ageN)

NotIn

findByAgeNotIn(Collection ages)

age NOT IN (age1, age2, ageN)

IsNotNull, NotNull

findByFirstnameNotNull()

firstname IS NOT NULL

IsNull, Null

findByFirstnameNull()

firstname IS NULL

Like, StartingWith, EndingWith

findByFirstnameLike(String name)

firstname LIKE name

NotLike, IsNotLike

findByFirstnameNotLike(String name)

firstname NOT LIKE name

Containing on String

findByFirstnameContaining(String name)

firstname LIKE '%' + name +'%'

NotContaining on String

findByFirstnameNotContaining(String name)

firstname NOT LIKE '%' + name +'%'

(No keyword)

findByFirstname(String name)

firstname = name

Not

findByFirstnameNot(String name)

firstname != name

IsTrue, True

findByActiveIsTrue()

active IS TRUE

IsFalse, False

findByActiveIsFalse()

active IS FALSE

Modifying Queries

The previous sections describe how to declare queries to access a given entity or collection of entities. Using keywords from the preceding table can be used in conjunction with delete…By or remove…By to create derived queries that delete matching rows.

Example 6. Delete…By Query
interface ReactivePersonRepository extends ReactiveSortingRepository<Person, String> {

  Mono<Integer> deleteByLastname(String lastname);            (1)

  Mono<Void> deletePersonByLastname(String lastname);         (2)

  Mono<Boolean> deletePersonByLastname(String lastname);      (3)
}
  1. Using a return type of Mono<Integer> returns the number of affected rows.

  2. Using Void just reports whether the rows were successfully deleted without emitting a result value.

  3. Using Boolean reports whether at least one row was removed.

As this approach is feasible for comprehensive custom functionality, you can modify queries that only need parameter binding by annotating the query method with @Modifying, as shown in the following example:

@Query("UPDATE person SET firstname = :firstname where lastname = :lastname")
Mono<Integer> setFixedFirstnameFor(String firstname, String lastname);

The result of a modifying query can be:

  • Void to discard update count and await completion.

  • Integer or another numeric type emitting the affected rows count.

  • Boolean to emit whether at least one row was updated.

The @Modifying annotation is only relevant in combination with the @Query annotation. Derived custom methods do not require this annotation.

Alternatively, you can add custom modifying behavior by using the facilities described in Custom Implementations for Spring Data Repositories.

Queries with SpEL Expressions

Query string definitions can be used together with SpEL expressions to create dynamic queries at runtime. SpEL expressions can provide predicate values which are evaluated right before executing the query.

Expressions expose method arguments through an array that contains all the arguments. The following query uses [0] to declare the predicate value for lastname (which is equivalent to the :lastname parameter binding):

public interface PersonRepository extends ReactiveCrudRepository<Person, String> {

  @Query("SELECT * FROM person WHERE lastname = :#{[0]} }")
  List<Person> findByQueryWithExpression(String lastname);
}

SpEL in query strings can be a powerful way to enhance queries. However, they can also accept a broad range of unwanted arguments. You should make sure to sanitize strings before passing them to the query to avoid unwanted changes to your query.

Expression support is extensible through the Query SPI: org.springframework.data.spel.spi.EvaluationContextExtension. The Query SPI can contribute properties and functions and can customize the root object. Extensions are retrieved from the application context at the time of SpEL evaluation when the query is built.

Tip
When using SpEL expressions in combination with plain parameters, use named parameter notation instead of native bind markers to ensure a proper binding order.

Entity State Detection Strategies

The following table describes the strategies that Spring Data R2DBC offers for detecting whether an entity is new:

Table 2. Options for detection whether an entity is new in Spring Data R2DBC

Id-Property inspection (the default)

By default, the save() method inspects the identifier property of the given entity. If the identifier property is null, then the entity is assumed to be new. Otherwise, it is assumed exist in the datbase.

Implementing Persistable

If an entity implements Persistable, Spring Data R2DBC delegates the new detection to the isNew(…) method of the entity. See the Javadoc for details.

Implementing EntityInformation

You can customize the EntityInformation abstraction used in SimpleR2dbcRepository by creating a subclass of R2dbcRepositoryFactory and overriding getEntityInformation(…). You then have to register the custom implementation of R2dbcRepositoryFactory as a Spring bean. Note that this should rarely be necessary. See the Javadoc for details.

ID Generation

Spring Data R2DBC uses the ID to identify entities. The ID of an entity must be annotated with Spring Data’s @Id annotation.

When your database has an auto-increment column for the ID column, the generated value gets set in the entity after inserting it into the database.

One important constraint is that, after saving an entity, the entity must not be new anymore. Note that whether an entity is new is part of the entity’s state. With auto-increment columns, this happens automatically, because the ID gets set by Spring Data with the value from the ID column.

Working with multiple Databases

When working with multiple, potentially different databases, your application will require a different approach to configuration. The provided AbstractR2dbcConfiguration support class assumes a single ConnectionFactory from which the Dialect gets derived. That being said, you need to define a few beans yourself to configure Spring Data R2DBC to work with multiple databases.

R2DBC repositories require either a DatabaseClient and ReactiveDataAccessStrategy or R2dbcEntityOperations to implement repositories. A simple configuration to scan for repositories without using AbstractR2dbcConfiguration looks like:

@Configuration
@EnableR2dbcRepositories(basePackages = "com.acme.mysql", entityOperationsRef = "mysqlR2dbcEntityOperations")
static class MySQLConfiguration {

    @Bean
    @Qualifier("mysql")
    public ConnectionFactory mysqlConnectionFactory() {
        return …;
    }

    @Bean
    public R2dbcEntityOperations mysqlR2dbcEntityOperations(@Qualifier("mysql") ConnectionFactory connectionFactory) {

        DefaultReactiveDataAccessStrategy strategy = new DefaultReactiveDataAccessStrategy(MySqlDialect.INSTANCE);
        DatabaseClient databaseClient = DatabaseClient.builder()
                .connectionFactory(connectionFactory)
                .dataAccessStrategy(strategy)
                .build();

        return new R2dbcEntityTemplate(databaseClient, strategy);
    }
}

Note that @EnableR2dbcRepositories allows configuration either through databaseClientRef or entityOperationsRef. Using various DatabaseClient beans is useful when connecting to multiple databases of the same type. When using different database systems that differ in their dialect, use @EnableR2dbcRepositories(entityOperationsRef = …)` instead.