Firestore with Spring Cloud GCP Firestore Data

Firestore with Spring Cloud GCP is awesome combination. If you still don’t think so, by the end of this article you will change your idea. In our previous article we used Firestore SDK to fetch and put data to the Firestore. Also we discussed about few other aspects such as query limitations, batch writes, asynchronous execution and so on. Here we are going to use Spring Cloud GCP Firstore Data project and use repository pattern to fetch and put data to the Firestore.

Setting up project

I think you have the project we used in first article about this duo. Add following dependency to the pom (or the gradle).

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

Then you can remove following two dependencies from the pom.

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

Spring Firestore Data Annotations

I hope you have experience of using spring data with relational databases or document databases such as mongo. Specially we had @Document and @DocumentId annotations to use with pojos when we had mongo as our data store. Same as mongo case here we have same two annotation @Document annotation for mark a pojo is mapped to a document in a particular collection. @DocumentId is used for mark a field to hold server generated unique id (By Firestore, an unique string). So let’s annotate User class with these two.

package com.aptkode.example.firestore;

import com.google.cloud.firestore.annotation.DocumentId;
import org.springframework.cloud.gcp.data.firestore.Document;

import java.util.List;

@Document(collectionName = "users")
public class User {
    @DocumentId
    private String name;
    private int age;
    private Gender gender;
    private List<String> interests;

    public User() {
    }

    public User(String name, int age, Gender gender, List<String> interests) {
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.interests = interests;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Gender getGender() {
        return gender;
    }

    public void setGender(Gender gender) {
        this.gender = gender;
    }

    public List<String> getInterests() {
        return interests;
    }

    public void setInterests(List<String> interests) {
        this.interests = interests;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", gender=" + gender +
                ", interests=" + interests +
                '}';
    }
}

Spring Firestore Data Repository Interface

Now let’s get into interesting part, the repository interfaces. I think you remember how we can throw out a method what we need to find and spring data magically convert it to underlying query implementation. And what we need to do is just have a component of repository and call the method, if we have data source setup, we can get results for the query. Same as that I am going to find users by their age.

package com.aptkode.example.firestore.repository;

import com.aptkode.example.firestore.User;
import org.springframework.cloud.gcp.data.firestore.FirestoreReactiveRepository;
import reactor.core.publisher.Flux;

public interface UserRepository extends FirestoreReactiveRepository<User> {

    Flux<User> findByAge(int age);

}

I think if you are not familiar with return type, you should have a look at my article about spring webflux. I am going to use it for next part. This is awesome, In previous article we wrote whole bunch of statements to get this thing done. Easy right. Let’s get our reactive web route live. You should have following dependency to use webflux.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

Handler, Router and WebClient

Then let’s have user handler, user router and web client ready. First the handler class, it has a method what we call a handler function to handle getUserByAge. First I have extracted the query parameter named age, convert it to an int then call the findByAge method of the user repository we just created. Second method I have here is to create a new user. As you can see, similar to other data repository implementations, default crud operations are available through parent interface. So we can easily use save method here to create new user. Then you have to follow similar mechanism as following to extract the user come with the payload, otherwise you will end up having threading issues due to the reactive nature. In all two methods response mapping is done in webflux functional reactive way.

package com.aptkode.example.firestore.handler;

import com.aptkode.example.firestore.User;
import com.aptkode.example.firestore.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

import javax.annotation.Nonnull;

@Component
public class UserHandler {

    @Autowired
    private UserRepository repository;

    @Nonnull
    public Mono<ServerResponse> getUsersByAge(ServerRequest request) {
        return request.queryParam("age")
                .map(Integer::valueOf)
                .map(age -> repository.findByAge(age))
                .map(users -> ServerResponse
                        .ok()
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(users, User.class))
                .orElse(
                        ServerResponse
                                .badRequest()
                                .contentType(MediaType.APPLICATION_JSON)
                                .build()
                );
    }

    @Nonnull
    public Mono<ServerResponse> save(ServerRequest request) {
        return request.bodyToMono(User.class)
                .flatMap(user ->
                        ServerResponse
                                .ok()
                                .contentType(MediaType.APPLICATION_JSON)
                                .body(repository.save(user), User.class)
                );
    }
}

Next we have router, it has two routes. One for get the users by some filtering criteria. In this case we have only getByAge, other filtering can be supported by updating the handler function. Next one for save a user.

package com.aptkode.example.firestore.router;

import com.aptkode.example.firestore.handler.UserHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

@Component
public class UserRouter {

    @Autowired
    private UserHandler userHandler;

    @Bean
    RouterFunction<ServerResponse> userRoute(){
        return RouterFunctions
                .route(RequestPredicates.GET("/users"), userHandler::getUsersByAge)
                .andRoute(RequestPredicates.POST("/user"), userHandler::save);
    }

}

Finally the webclient.

package com.aptkode.example.firestore;

import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutures;
import com.google.cloud.firestore.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import java.util.stream.Collectors;

@SpringBootApplication
public class FirestoreApplication {

    private final static Logger logger = LoggerFactory.getLogger(FirestoreApplication.class);

    @Autowired
    private Firestore firestore;

    public static void main(String[] args) {
        SpringApplication.run(FirestoreApplication.class, args);
        WebClient.create("http://localhost:8080");
    }
}

Final Thoughts

Now give it a go, create a user. Find the user by age. This is far more less code comparing to the use of SDK methods directly. So I think you have learned something valuable, don’t forget to apply these in your day to day life. Also try out other difficult scenarios with this such as range query with limits, I’ll share my experience on those in future article. This code will be available in github if you want to hack around it. Happy Coding.

Leave a Comment

Your email address will not be published. Required fields are marked *