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 Firestore 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

Supported types

You may use the following field types when defining your persistent entities or when binding query parameters:

  • Long

  • Integer

  • Double

  • Float

  • String

  • Boolean

  • Character

  • Date

  • Map

  • List

  • Enum

  • com.google.cloud.Timestamp

  • com.google.cloud.firestore.GeoPoint

  • com.google.cloud.firestore.Blob

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 com.google.cloud.firestore.annotation.DocumentId;
import org.springframework.cloud.gcp.data.firestore.Document;

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

	private Integer age;

	public User() {
	}

	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Integer getAge() {
		return this.age;
	}

	public void setAge(Integer age) {
		this.age = 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.

Embedded entities and lists

Spring Data Cloud Firestore supports embedded properties of custom types and lists. Given a custom POJO definition, you can have properties of this type or lists of this type in your entities. They are stored as embedded documents (or arrays, correspondingly) in the Cloud Firestore.

Example:

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

	private Integer age;

	private List<String> pets;

	private List<Address> addresses;

	private Address homeAddress;

	public List<String> getPets() {
		return this.pets;
	}

	public void setPets(List<String> pets) {
		this.pets = pets;
	}

	public List<Address> getAddresses() {
		return this.addresses;
	}

	public void setAddresses(List<Address> addresses) {
		this.addresses = addresses;
	}

	public Address getHomeAddress() {
		return this.homeAddress;
	}

	public void setHomeAddress(Address homeAddress) {
		this.homeAddress = homeAddress;
	}

	public static class Address {
		String streetAddress;
		String country;

		public Address() {
		}
	}
}

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);

	Flux<User> findByAgeIn(List<Integer> ages);

	Flux<User> findByAgeAndPetsContains(Integer age, List<String> pets);

	Flux<User> findByPetsContains(List<String> pets);

	Flux<User> findByPetsContainsAndAgeIn(String pets, List<Integer> ages);

	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() {
		List<User.Address> addresses = Arrays.asList(new User.Address("123 Alice st", "US"),
				new User.Address("1 Alice ave", "US"));
		User.Address homeAddress = new User.Address("10 Alice blvd", "UK");
		User alice = new User("Alice", 29, null, addresses, homeAddress);
		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");

		User aliceLoaded = this.userRepository.findById("Alice").block();
		assertThat(aliceLoaded.getAddresses()).isEqualTo(addresses);
		assertThat(aliceLoaded.getHomeAddress()).isEqualTo(homeAddress);
	}
}

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

  • contains (accepts a List with up to 10 elements, or a singular value)

  • in (accepts a List with up to 10 elements)

If in operation is used in combination with contains operation, the argument to contains operation has to be a singular value.

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.

Declarative Transactions with @Transactional Annotation

This feature requires a bean of SpannerTransactionManager, which is provided when using spring-cloud-gcp-starter-data-firestore.

FirestoreTemplate and FirestoreReactiveRepository support running methods with the @Transactional annotation as transactions. If a method annotated with @Transactional calls another method also annotated, then both methods will work within the same transaction.

One way to use this feature is illustrated here. You would need to do the following:

  1. Annotate your configuration class with the @EnableTransactionManagement annotation.

  2. Create a service class that has methods annotated with @Transactional:

class UserService {
	@Autowired
	private UserRepository userRepository;

	@Transactional
	public Mono<Void> updateUsers() {
		return this.userRepository.findAll()
				.flatMap(a -> {
					a.setAge(a.getAge() - 1);
					return this.userRepository.save(a);
				})
				.then();
	}
}
  1. Make a Spring Bean provider that creates an instance of that class:

@Bean
public UserService userService() {
	return new UserService();
}

After that, you can autowire your service like so:

public class MyApplication {
	@Autowired
	UserService userService;
}

Now when you call the methods annotated with @Transactional on your service object, a transaction will be automatically started. If an error occurs during the execution of a method annotated with @Transactional, the transaction will be rolled back. If no error occurs, the transaction will be committed.

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.