Search results
Writing dynamic Cosmos DB queries using Spring Data Cosmos repositories and ReactiveCosmosTemplate
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:
- Writing dynamic SQL queries using Spring Data JPA repositories and EntityManager
- Writing dynamic SQL queries using Spring Data JPA Specification and Criteria API
- Writing dynamic SQL queries using Spring Data JPA repositories and Querydsl
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 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.
Read on if you are interested in learning how to extract a certificate and add it to a Truststore programmatically for your Spring Boot applications to connect to the Cosmos DB emulator Docker container.
You can also go over these two relevant code snippets:
Create KeyStore / Truststore Programmatically.
Add Cosmos DB emulator Certificate to TrustStore via Bash Shell Script.
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>
...
-
This demo Spring Boot application uses version
2.7.14
. -
Azure BOM’s
spring-cloud-azure-dependencies
version is set to4.10.0
. This is the highest supported version at the time of this writing that you could use with Spring Boot 2.7.x. It manages a number of Spring, Azure-related libraries.
spring-cloud-azure-starter-data-cosmos
is one of those libraries managed byspring-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 usespring-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.
This blog post uses ReactiveCosmosTemplate in the callback interface to write dynamic Cosmos queries.
You could also use CosmosTemplate, CosmosClient, CosmosAsyncClient.
Your selection depends on what you need, and what methods these libraries provide.
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;
}
});
}
}
To keep this blog post simple,
searchCriteria
attributes are not escaped. This makes your Spring Boot applications vulnerable to SQL/NoSQL injection.
It’s a good practice to use a library like coverity-escapers to prevent SQL injection as well as other OWASP vulnerabilities.
-
@Repository-annotated interface extends AsimioReactiveCosmosRepository interface. It allows you to execute
findAll()
method passing an implementation of the ReactiveCosmosQueryCallback interface, either using Lambda or an anonymous inner implementation. -
The ReactiveCosmosQueryCallback implementation builds a String with the Cosmos DB query.
The query will include only those conditions where the associated UserSearchCriteria attributes are found.
The callback implementation then uses this query to instantiate a Cosmos SqlQuerySpec object, which is then passed into the ReactiveCosmosTemplate’srunQuery()
method.
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
For environments other than local development, you might want to externalize and secure sensitive data such as the Cosmos DB key.
You could use Spring Cloud Config Server or Azure KeyVault.
Stay tuned, I’ll cover how to store Cosmos DB credentials in Azure KeyVault in another blog post.
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.
Read on to learn:
How to seed Cosmos DB data to run Spring Boot 2 Integration Tests.
How to write integration tests with Testcontainers and the Azure Cosmos DB Docker emulator for Spring Boot applications.
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
- https://learn.microsoft.com/en-us/azure/cosmos-db/docker-emulator-linux
- https://learn.microsoft.com/en-us/azure/cosmos-db/local-emulator-export-ssl-certificates
- https://github.com/Azure/azure-sdk-for-java/wiki/Spring-Versions-Mapping#which-version-of-spring-cloud-azure-should-i-use
- https://learn.microsoft.com/en-us/azure/developer/java/spring-framework/developer-guide-overview