Persistence top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we'll learn how to configure and implement database operations in a reactive way on Couchbase using Spring Data Repositories.

We'll cover the basic usages of ReactiveCrudRepository and ReactiveSortingRepository. Additionally, we'll configure our test application with AbstractReactiveCouchbaseConfiguration.

2. Maven Dependencies

Firstly, let's add the necessary dependencies:

<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-core</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-couchbase-reactive</artifactId>
</dependency>

The spring-boot-starter-data-couchbase-reactive dependency contains everything that we need to operate on Couchbase using reactive API.

We'll also include the reactor-core dependency to use Project Reactor API.

3. Configuration

Next, let's define the connection settings between Couchbase and our application.

Let's begin by creating a class that will hold our properties:

@Configuration
public class CouchbaseProperties {

    private List<String> bootstrapHosts;
    private String bucketName;
    private String bucketPassword;
    private int port;

    public CouchbaseProperties(
      @Value("${spring.couchbase.bootstrap-hosts}") List<String> bootstrapHosts, 
      @Value("${spring.couchbase.bucket.name}") String bucketName, 
      @Value("${spring.couchbase.bucket.password}") String bucketPassword, 
      @Value("${spring.couchbase.port}") int port) {
        this.bootstrapHosts = Collections.unmodifiableList(bootstrapHosts);
        this.bucketName = bucketName;
        this.bucketPassword = bucketPassword;
        this.port = port;
    }

    // getters
}

In order to use reactive support, we should create the configuration class that'll extend AbstractReactiveCouchbaseConfiguration:

@Configuration
@EnableReactiveCouchbaseRepositories("com.baeldung.couchbase.domain.repository")
public class ReactiveCouchbaseConfiguration extends AbstractReactiveCouchbaseConfiguration {

    private CouchbaseProperties couchbaseProperties;

    public ReactiveCouchbaseConfiguration(CouchbaseProperties couchbaseProperties) {
        this.couchbaseProperties = couchbaseProperties;
    }

    @Override
    protected List<String> getBootstrapHosts() {
        return couchbaseProperties.getBootstrapHosts();
    }

    @Override
    protected String getBucketName() {
        return couchbaseProperties.getBucketName();
    }

    @Override
    protected String getBucketPassword() {
        return couchbaseProperties.getBucketPassword();
    }

    @Override
    public CouchbaseEnvironment couchbaseEnvironment() {
        return DefaultCouchbaseEnvironment
          .builder()
          .bootstrapHttpDirectPort(couchbaseProperties.getPort())
          .build();
    }
}

Additionally, we've used @EnableReactiveCouchbaseRepositories to enable our reactive repositories that'll be under the specified package.

Furthermore, we've overridden couchbaseEnvironment() in order to pass the Couchbase connection port.

4. Repositories

In this section, we'll learn how to create and use the reactive repository. By default, the “all” view is backing most CRUD operations. The custom repository methods are backed by N1QL. If the cluster doesn't support N1QL, the UnsupportedCouchbaseFeatureException will be thrown during initialization.

Firstly, let's create the POJO class that our repositories will work with:

@Document
public class Person {
    @Id private UUID id;
    private String firstName;

   //getters and setters
}

4.1. View-Based Repository

Now, we'll create a repository for Person:

@Repository
@ViewIndexed(designDoc = ViewPersonRepository.DESIGN_DOCUMENT)
public interface ViewPersonRepository extends ReactiveCrudRepository<Person, UUID> {

    String DESIGN_DOCUMENT = "person";
}

The repository extends the ReactiveCrudRepository interface in order to use Reactor API to interact with Couchbase.

Additionally, we can add a custom method and use the @View annotation to make it view-based:

@View(designDocument = ViewPersonRepository.DESIGN_DOCUMENT)
Flux<Person> findByFirstName(String firstName);

By default, the query will look for a view named byFirstName. If we want to provide a custom view name, we'll have to use the viewName argument.

Lastly, let's create a simple CRUD test with the help of a test subscriber:

@Test
public void shouldSavePerson_findById_thenDeleteIt() {
    final UUID id = UUID.randomUUID();
    final Person person = new Person(id, "John");
    personRepository
      .save(person)
      .subscribe();
 
    final Mono<Person> byId = personRepository.findById(id);
 
    StepVerifier
      .create(byId)
      .expectNextMatches(result -> result
        .getId()
        .equals(id))
      .expectComplete()
      .verify();
 
    personRepository
      .delete(person)
      .subscribe();
}

4.2. N1QL/View-Based Repository

Now, we'll create the reactive repository for Person that'll use the N1QL queries:

@Repository
@N1qlPrimaryIndexed
public interface N1QLPersonRepository extends ReactiveCrudRepository<Person, UUID> {
    Flux<Person> findAllByFirstName(String firstName);
}

The repository extends the ReactiveCrudRepository in order to use Reactor API as well. In addition, we've added a custom findAllByFirstName method, which creates the N1QL backed query.

After that, let's add the test for the findAllByFirstName method:

@Test
public void shouldFindAll_byLastName() {
    final String firstName = "John";
    final Person matchingPerson = new Person(UUID.randomUUID(), firstName);
    final Person nonMatchingPerson = new Person(UUID.randomUUID(), "NotJohn");
    personRepository
      .save(matchingPerson)
      .subscribe();
    personRepository
      .save(nonMatchingPerson)
      .subscribe();
 
    final Flux<Person> allByFirstName = personRepository.findAllByFirstName(firstName);
 
    StepVerifier
      .create(allByFirstName)
      .expectNext(matchingPerson)
      .verifyComplete();
}

Furthermore, we'll create a repository that allows us to retrieve people using the sorting abstraction:

@Repository
public interface N1QLSortingPersonRepository extends ReactiveSortingRepository<Person, UUID> {
    Flux<Person> findAllByFirstName(String firstName, Sort sort);
}

Lastly, let's write a test to check if the data is actually sorted:

@Test
public void shouldFindAll_sortedByFirstName() {
    final Person firstPerson = new Person(UUID.randomUUID(), "John");
    final Person secondPerson = new Person(UUID.randomUUID(), "Mikki");
    personRepository
      .save(firstPerson)
      .subscribe();
    personRepository
      .save(secondPerson)
      .subscribe();
 
    final Flux<Person> allByFirstName = personRepository
      .findAll(Sort.by(Sort.Direction.DESC, "firstName"));
 
    StepVerifier
      .create(allByFirstName)
      .expectNextMatches(person -> person
        .getFirstName()
        .equals(secondPerson.getFirstName()))
      .expectNextMatches(person -> person
        .getFirstName()
        .equals(firstPerson.getFirstName()))
      .verifyComplete();
}

5. Conclusion

In this article, we've learned how to use repositories using reactive programming with Couchbase and Spring Data Reactive framework.

As always, the code for these examples is available over on Github.

Persistence bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

Leave a Reply

avatar
  Subscribe  
Notify of