1. OVERVIEW

Let’s say you need to write a RESTful endpoint that takes a number of request parameters, and use them to filter out data from a database.

Something like:

/api/users?firstName=...&lastName=...

Some of the request parameters are optional, so you would only include query conditions in each SQL statement depending on the request parameters sent with each request.

You’ll be writing dynamic SQL queries.

I have covered different solutions when the data comes from a relational database:

But what if the data store is not a relational database? What if the data store is a NoSQL database?

More specifically, what if the database is Azure Cosmos DB?

This tutorial teaches you how to extend Spring Data Cosmos for your repositories to access the ReactiveCosmosTemplate so that you can write dynamic Cosmos DB queries.

Spring Data and Azure Cosmos DB Spring Data Cosmos

2. COSMOS DB EMULATOR

This blog post uses Microsoft’s azure-cosmos-emulator Docker image.

It assumes your Spring Boot application already connects to a Cosmos DB container, located either in an emulator Docker container, in the native Windows emulator, or hosted in Azure.

3. MAVEN DEPENDENCIES

pom.xml:

...
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.7.14</version>
  <relativePath />
</parent>
...
<properties>
  <java.version>11</java.version>
  <spring-cloud-azure.version>4.10.0</spring-cloud-azure.version>
</properties>

<dependencies>
  <dependency>
    <groupId>com.azure.spring</groupId>
    <artifactId>spring-cloud-azure-starter-data-cosmos</artifactId>
  </dependency>
</dependencies>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.azure.spring</groupId>
      <artifactId>spring-cloud-azure-dependencies</artifactId>
      <version>${spring-cloud-azure.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
...
  • spring-cloud-azure-starter-data-cosmos is one of those libraries managed by spring-cloud-azure-dependencies, and the one we are going to need to implement CRUD against a Cosmos DB.
    You would use it similarly to how you would use spring-boot-starter-data-jpa with a relational database.

4. CONTAINER DOCUMENTS

User.java:

@Container(containerName = "users")
public class User {

  @Id
  private String id;
  private String firstName;
  private String lastName;
  private String address;
// ...
}

users is the name of the container space in the sampleDb Cosmos database. It’ll store JSON documents corresponding to User.java POJO instances.

5. EXTENDING SPRING DATA COSMOS

Influenced by Spring Framework’s Template with Callback design pattern implementations such as TransactionTemplate, which uses TransactionCallback; let’s define a callback interface first.

ReactiveCosmosQueryCallback.java:

public interface ReactiveCosmosQueryCallback<T> {

  CorePublisher<T> doWithReactiveCosmosTemplate(ReactiveCosmosTemplate cosmosTemplate);

}

This is the interface your Spring Data Cosmos-based repositories would need to implement to access a ReactiveCosmosTemplate instance to build dynamic Cosmos DB queries.

Next, let’s define a custom Spring Data Cosmos repository base interface.

AsimioReactiveCosmosRepository.java:

@NoRepositoryBean
public interface AsimioReactiveCosmosRepository {

  <T> Flux<T> findAll(ReactiveCosmosQueryCallback<T> callback);

}

This is the interface, or one of the interfaces your Spring Data Cosmos repositories would need to extend from.

@NoRepositoryBean prevents the AsimioReactiveCosmosRepository intermediate interface from being proxied. It’s used when providing a base interface with new methods for your repositories along with the custom base repository implementation.

Let’s implement this interface next.

AsimioReactiveCosmosRepositoryImpl.java:

public class AsimioReactiveCosmosRepositoryImpl<E, ID extends Serializable> extends SimpleReactiveCosmosRepository<E, ID>
    implements AsimioReactiveCosmosRepository {

  protected final ReactiveCosmosTemplate cosmosTemplate;

  public AsimioReactiveCosmosRepositoryImpl(CosmosEntityInformation<E, ID> metadata, ReactiveCosmosTemplate cosmosTemplate) {
    super(metadata, cosmosTemplate);
    this.cosmosTemplate = cosmosTemplate;
  }

  @Override
  public <T> Flux<T> findAll(ReactiveCosmosQueryCallback<T> callback) {
    return (Flux<T>) callback.doWithReactiveCosmosTemplate(this.cosmosTemplate);
  }
// ...
}
  • cosmosTemplate attribute is passed through the callback implementation for a Cosmos-based @Repository method to create dynamic Cosmos queries.
  • findAll() method executes the callback implemented by a specific Cosmos-based @Repository.

6. SPRING DATA COSMOS REPOSITORIES

Let’s implement the User repository and write a dynamic Cosmos query to retrieve Users based on the presence of search criteria attributes.

UserRepository.java:

@Repository
public interface UserRepository extends ReactiveCosmosRepository<User, String>, AsimioReactiveCosmosRepository {

  default Flux<User> findAll(UserSearchCriteria searchCriteria) {
    return this.findAll(new ReactiveCosmosQueryCallback<User>() {

      @Override
      public Flux<User> doWithReactiveCosmosTemplate(ReactiveCosmosTemplate cosmosTemplate) {
        Map<String, Object> queryParams = this.buildQueryParameters();

        StringBuilder builder = new StringBuilder();
        // SELECT
        builder.append("SELECT * " + System.lineSeparator());

        // FROM
        builder.append("FROM User u " + System.lineSeparator());

        // WHERE
        if (queryParams.size() > 0) {
          boolean appendOrOperator = false;
          builder.append("WHERE " + System.lineSeparator());
          if (queryParams.get("@firstName") != null) {
            appendOrOperator = true;
            builder.append("  u.firstName = @firstName " + System.lineSeparator());
          }
          if (queryParams.get("@lastName") != null) {
            if (appendOrOperator) {
              builder.append("  OR");
            }
            builder.append("  u.lastName = @lastName " + System.lineSeparator());
          }
        }

        // Creates query parameters
        List<SqlParameter> parameters = queryParams.entrySet().stream()
          .map(entry -> new SqlParameter(entry.getKey(), entry.getValue()))
          .collect(Collectors.toList());

        // Create query
        SqlQuerySpec querySpec = new SqlQuerySpec(builder.toString(), parameters);
        Flux<User> users = cosmosTemplate.runQuery(querySpec, User.class, User.class);

        return users;
      }

      private Map<String, Object> buildQueryParameters() {
        Map<String, Object> result = Maps.newHashMap();
        if (StringUtils.isNotEmpty(searchCriteria.getFirstName())) {
          result.put("@firstName", searchCriteria.getFirstName());
        }
        if (StringUtils.isNotEmpty(searchCriteria.getLastName())) {
          result.put("@lastName", searchCriteria.getLastName());
        }
        return result;
      }

    });
  }

}

6.1. CONFIGURATION

You still need to let know Spring Data Cosmos to use your custom repository class: AsimioReactiveCosmosRepositoryImpl.

Application.java:

@SpringBootApplication
@EnableReactiveCosmosRepositories(
  repositoryBaseClass = AsimioReactiveCosmosRepositoryImpl.class
)
public class Application {
// ...
}

This application includes @EnableReactiveCosmosRepositories annotation because the UserRepository extends from ReactiveCosmosRepository. Had it extended from CosmosRepository then the application would have included @EnableCosmosRepositories instead.

And the properties configuration:

application.yml:

spring:
  cloud:
    azure:
      cosmos:
        endpoint: https://localhost:8081
        key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
        database: sampleDb

You would get the Cosmos key from the emulator home page.

Cosmos DB Emulator Key and Connection Strings Cosmos DB Emulator Key and Connection Strings

7. SERVICE CLASS

@Service
public class DefaultProvisioningService implements ProvisioningService {

  private final UserRepository userRepository;

  @Override
  public List<User> retrieveUsers(UserSearchCriteria searchCriteria) {
    Flux<User> result = this.userRepository.findAll(searchCriteria);
    return result.collectList().block();
  }
// ...
}

The searchCriteria argument instantiated in the REST Controller is passed to the User repository method.

UserSearchCriteria.java:

public class UserSearchCriteria {

  private String firstName;
  private String lastName;
// ...
}

UserSearchCriteria is a wrapper class to hold the request parameters passed in the request endpoint.

8. DYNAMIC QUERIES IN ACTION

  • No request parameter to filter from. Returns all Users:
curl http://localhost:8080/api/users | json_pp
[
  {
    "address" : "Some address",
    "firstName" : "Orlando",
    "id" : "1",
    "lastName" : "Otero"
  },
  {
    "address" : "Another address",
    "firstName" : "Blah",
    "id" : "2",
    "lastName" : "Meh"
  }
]
  • Filters Users by the firstName request parameter:
curl http://localhost:8080/api/users?firstName=Orlando | json_pp
[
  {
    "address" : "Some address",
    "firstName" : "Orlando",
    "id" : "1",
    "lastName" : "Otero"
  }
]
  • Filter Users by firstName OR lastName request parameters:
curl "http://localhost:8080/api/users?firstName=Orlando&lastName=Meh" | json_pp
[
  {
    "address" : "Some address",
    "firstName" : "Orlando",
    "id" : "1",
    "lastName" : "Otero"
  },
  {
    "address" : "Another address",
    "firstName" : "Blah",
    "id" : "2",
    "lastName" : "Meh"
  }
]

9. CONCLUSION

This blog post covered how to write dynamic Cosmos DB queries in your Spring Data Cosmos repositories using the ReactiveCosmosTemplate.

You can extend Spring Data Cosmos and add support to write dynamic Cosmos queries in your repositories.

This approach is not restricted to ReactiveCosmosTemplate, you could also use CosmosTemplate, CosmosClient, and CosmosAsyncClient to write dynamic Cosmos queries.

Thanks for reading and as always, feedback is very much appreciated. If you found this post helpful and would like to receive updates when content like this gets published, sign up to the newsletter.

10. SOURCE CODE

Accompanying source code for this blog post can be found at:

11. REFERENCES