Spring Data Reactive Repositories for Cloud Firestore

Currently some features are not supported: transactions, sorting, query by example, projections, auditing.

Spring Data is an abstraction for storing and retrieving POJOs in numerous storage technologies. Spring Cloud GCP adds Spring Data support for Google Cloud Firestore in native mode, providing reactive template and repositories support. To begin using this library, add the spring-cloud-gcp-data-firestore artifact to your project.

Maven coordinates for this module only, using Spring Cloud GCP BOM:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-gcp-data-firestore</artifactId>
</dependency>

Gradle coordinates:

dependencies {
  compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-data-firestore'
}

We provide a Spring Boot Starter for Spring Data Firestore, with which you can use our recommended auto-configuration setup. To use the starter, see the coordinates below.

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-gcp-starter-data-firestore</artifactId>
</dependency>

Gradle coordinates:

dependencies {
  compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-data-firestore'
}

Configuration

Properties

The Spring Boot starter for Google Cloud Firestore provides the following configuration options:

Name

Description

Required

Default value

spring.cloud.gcp.firestore.enabled

Enables or disables Pub/Sub auto-configuration

No

true

spring.cloud.gcp.firestore.project-id

GCP project ID where the Google Cloud Firestore API is hosted, if different from the one in the Spring Cloud GCP Core Module

No

spring.cloud.gcp.firestore.credentials.location

OAuth2 credentials for authenticating with the Google Cloud Firestore API, if different from the ones in the Spring Cloud GCP Core Module

No

spring.cloud.gcp.firestore.credentials.encoded-key

Base64-encoded OAuth2 credentials for authenticating with the Google Cloud Firestore API, if different from the ones in the Spring Cloud GCP Core Module

No

spring.cloud.gcp.firestore.credentials.scopes

OAuth2 scope for Spring Cloud GCP Cloud Firestore credentials

No

https://www.googleapis.com/auth/datastore

Reactive Repository settings

Spring Data Repositories can be configured via the @EnableReactiveFirestoreRepositories annotation on your main @Configuration class. With our Spring Boot Starter for Spring Data Cloud Firestore, @EnableReactiveFirestoreRepositories is automatically added. It is not required to add it to any other class, unless there is a need to override finer grain configuration parameters provided by @EnableReactiveFirestoreRepositories.

Autoconfiguration

Our Spring Boot autoconfiguration creates the following beans available in the Spring application context:

  • an instance of FirestoreTemplate

  • instances of all user defined repositories extending FirestoreReactiveRepository (an extension of ReactiveCrudRepository with additional Cloud Firestore features) when repositories are enabled

  • an instance of Firestore from the Google Cloud Java Client for Firestore, for convenience and lower level API access

Object Mapping

Spring Data Cloud Firestore allows you to map domain POJOs to Cloud Firestore collections and documents via annotations:

import org.springframework.data.annotation.Id;
import org.springframework.cloud.gcp.data.firestore.Document;

@Document(collectionName = "usersCollection")
public class User {
	@DocumentId
	private String name;

	private Integer age;
}

@Document(collectionName = "usersCollection") annotation configures the collection name for the documents of this type. This annotation is optional, by default the collection name is derived from the class name.

@DocumentId annotation marks a field to be used as document id. This annotation is required.

Internally we use Firestore client library object mapping. See the documentation for supported annotations.

Reactive Repositories

Spring Data Repositories is an abstraction that can reduce boilerplate code.

For example:

public interface UserRepository extends FirestoreReactiveRepository<User> {
	Flux<User> findByAge(Integer age);

	Flux<User> findByAgeGreaterThanAndAgeLessThan(Integer age1, Integer age2);

	Flux<User> findByAgeGreaterThan(Integer age);

	Flux<User> findByAgeGreaterThan(Integer age, Pageable pageable);

	Mono<Long> countByAgeIsGreaterThan(Integer age);
}

Spring Data generates a working implementation of the specified interface, which can be autowired into an application.

The User type parameter to FirestoreReactiveRepository refers to the underlying domain type.

public class MyApplication {

	@Autowired
	UserRepository userRepository;

	public void writeReadDeleteTest() {
		User alice = new User("Alice", 29);
		User bob = new User("Bob", 60);

		this.userRepository.save(alice).block();
		this.userRepository.save(bob).block();

		assertThat(this.userRepository.count().block()).isEqualTo(2);
		assertThat(this.userRepository.findAll().map(User::getName).collectList().block())
				.containsExactlyInAnyOrder("Alice", "Bob");
	}
}

Repositories allow you to define custom Query Methods (detailed in the following sections) for retrieving and counting based on filtering and paging parameters.

Custom queries with @Query annotation are not supported since there is no query language in Cloud Firestore

Query methods by convention

public class MyApplication {
	public void partTreeRepositoryMethodTest() {
		User u1 = new User("Cloud", 22);
		User u2 = new User("Squall", 17);
		Flux<User> users = Flux.fromArray(new User[] { u1, u2 });

		this.userRepository.saveAll(users).blockLast();

		assertThat(this.userRepository.count().block()).isEqualTo(2);
		assertThat(this.userRepository.findByAge(22).collectList().block()).containsExactly(u1);
		assertThat(this.userRepository.findByAgeGreaterThanAndAgeLessThan(20, 30).collectList().block())
				.containsExactly(u1);
		assertThat(this.userRepository.findByAgeGreaterThan(10).collectList().block()).containsExactlyInAnyOrder(u1,
				u2);
	}
}

In the example above the query method implementations in UserRepository are generated based on the name of the methods using the Spring Data Query creation naming convention.

Cloud Firestore only supports filter components joined by AND, and the following operations:

  • equals

  • greater than or equals

  • greater than

  • less than or equals

  • less than

  • is null

After writing a custom repository interface specifying just the signatures of these methods, implementations are generated for you and can be used with an auto-wired instance of the repository.

Transactions

Read-only and read-write transactions are provided by TransactionalOperator (see this blog post on reactive transactions for details). In order to use it, you would need to autowire ReactiveFirestoreTransactionManager like this:

public class MyApplication {
	@Autowired
	ReactiveFirestoreTransactionManager txManager;
}

After that you will be able to use it to create an instance of TransactionalOperator. Note that you can switch between read-only and read-write transactions using TransactionDefinition object:

DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
transactionDefinition.setReadOnly(false);
TransactionalOperator operator = TransactionalOperator.create(this.txManager, transactionDefinition);

When you have an instance of TransactionalOperator, you can execute a sequence of Firestore operations in a transaction using operator::transactional:

User alice = new User("Alice", 29);
User bob = new User("Bob", 60);

this.userRepository.save(alice)
		.then(this.userRepository.save(bob))
		.as(operator::transactional)
		.block();

this.userRepository.findAll()
		.flatMap(a -> {
			a.setAge(a.getAge() - 1);
			return this.userRepository.save(a);
		})
		.as(operator::transactional).collectList().block();

assertThat(this.userRepository.findAll().map(User::getAge).collectList().block())
		.containsExactlyInAnyOrder(28, 59);
Read operations in a transaction can only happen before write operations. All write operations are applied atomically. Read documents are locked until the transaction finishes with a commit or a rollback, which are handled by Spring Data. If an Exception is thrown within a transaction, the rollback operation is executed. Otherwise, the commit operation is executed.

Cloud Firestore Spring Boot Starter

If you prefer using Firestore client only, Spring Cloud GCP provides a convenience starter which automatically configures authentication settings and client objects needed to begin using Google Cloud Firestore in native mode.

See documentation to learn more about Cloud Firestore.

To begin using this library, add the spring-cloud-gcp-starter-firestore artifact to your project.

Maven coordinates, using Spring Cloud GCP BOM:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-gcp-starter-firestore</artifactId>
</dependency>

Gradle coordinates:

dependencies {
  compile group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter-firestore'
}

Using Cloud Firestore

The starter automatically configures and registers a Firestore bean in the Spring application context. To start using it, simply use the @Autowired annotation.

@Autowired
Firestore firestore;

void writeDocumentFromObject() throws ExecutionException, InterruptedException {
	// Add document data with id "joe" using a custom User class
	User data = new User("Joe",
			Arrays.asList(
					new Phone(12345, PhoneType.CELL),
					new Phone(54321, PhoneType.WORK)));

	// .get() blocks on response
	WriteResult writeResult = this.firestore.document("users/joe").set(data).get();

	LOGGER.info("Update time: " + writeResult.getUpdateTime());
}

User readDocumentToObject() throws ExecutionException, InterruptedException {
		ApiFuture<DocumentSnapshot> documentFuture =
				this.firestore.document("users/joe").get();

		User user = documentFuture.get().toObject(User.class);

		LOGGER.info("read: " + user);

		return user;
}

Sample

A sample application is available.