Spring Boot application with Cloud Datastore

1. Overview

Google Cloud Datastore is a NoSQL document database built for automatic scaling, high performance, and ease of application development.

What you'll learn

  • How to use Cloud Datastore to save and retrieve java objects in Spring Boot

What you'll need

  • A Google Cloud Platform Project
  • A Browser, such as Chrome or Firefox

How will you use this tutorial?

Read it through only Read it and complete the exercises

How would you rate your experience with using Google Cloud Platform services?

Novice Intermediate Proficient

2. Setup and Requirements

Self-paced environment setup

  1. Sign-in to the Google Cloud Console and create a new project or reuse an existing one. If you don't already have a Gmail or Google Workspace account, you must create one.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • The Project name is the display name for this project's participants. It is a character string not used by Google APIs. You can always update it.
  • The Project ID is unique across all Google Cloud projects and is immutable (cannot be changed after it has been set). The Cloud Console auto-generates a unique string; usually you don't care what it is. In most codelabs, you'll need to reference your Project ID (typically identified as PROJECT_ID). If you don't like the generated ID, you might generate another random one. Alternatively, you can try your own, and see if it's available. It can't be changed after this step and remains for the duration of the project.
  • For your information, there is a third value, a Project Number, which some APIs use. Learn more about all three of these values in the documentation.
  1. Next, you'll need to enable billing in the Cloud Console to use Cloud resources/APIs. Running through this codelab won't cost much, if anything at all. To shut down resources to avoid incurring billing beyond this tutorial, you can delete the resources you created or delete the project. New Google Cloud users are eligible for the $300 USD Free Trial program.

Activate Cloud Shell

  1. From the Cloud Console, click Activate Cloud Shell 853e55310c205094.png.

55efc1aaa7a4d3ad.png

If this is your first time starting Cloud Shell, you're presented with an intermediate screen describing what it is. If you were presented with an intermediate screen, click Continue.

9c92662c6a846a5c.png

It should only take a few moments to provision and connect to Cloud Shell.

9f0e51b578fecce5.png

This virtual machine is loaded with all the development tools needed. It offers a persistent 5 GB home directory and runs in Google Cloud, greatly enhancing network performance and authentication. Much, if not all, of your work in this codelab can be done with a browser.

Once connected to Cloud Shell, you should see that you are authenticated and that the project is set to your project ID.

  1. Run the following command in Cloud Shell to confirm that you are authenticated:
gcloud auth list

Command output

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Run the following command in Cloud Shell to confirm that the gcloud command knows about your project:
gcloud config list project

Command output

[core]
project = <PROJECT_ID>

If it is not, you can set it with this command:

gcloud config set project <PROJECT_ID>

Command output

Updated property [core/project].

3. Initialize Cloud Datastore

In GCP Console, navigate to Menu -> Datastore (in the Storage section) or click here.

If you've never used Datastore in the current project, you will see the "Select a Cloud Firestore mode" screen. Select the "Datastore mode" option.

f938295c7ff297f4.png

After that, you will see "Choose where to store your data" screen. Select us-east1 or any other regional location and click "Create Database":

916ac84fec10fae7.png

4. Bootstrap a new Spring Boot Java Application

From the CloudShell environment, use the following command to initialize and bootstrap a new Spring Boot application:

$ curl https://start.spring.io/starter.tgz \
  -d packaging=war \
  -d dependencies=cloud-gcp \
  -d type=maven-project \
  -d baseDir=datastore-example \
  -d bootVersion=3.0.5 | tar -xzvf -

This will create a new datastore-example/ directory with a new Maven project, along with Maven's pom.xml, a Maven wrapper, and an application entrypoint.

Our application will provide a CLI for users to enter commands and see results. We will create a class to represent a book and then will save it to Cloud Datastore using Datastore Repository.

We also need to add one more necessary dependency to the pom.xml.

Open the Web Code Editor by clicking Open editor from the Cloud Shell menu.

6d823258c76a7452.png

After the editor loads, modify the pom.xml file to add the Google Cloud Datastore Starter and Spring Shell Starter dependencies:

pom.xml

<project>
  ...
  <dependencies>
        ...
        <!-- Add GCP Datastore Starter -->
        <dependency>
                <groupId>com.google.cloud</groupId>
                <artifactId>spring-cloud-gcp-starter-data-datastore</artifactId>
        </dependency>

        <!-- Add Spring Shell Starter -->
        <dependency>
                <groupId>org.springframework.shell</groupId>
                <artifactId>spring-shell-starter</artifactId>
                <version>3.0.2</version>
        </dependency>

  </dependencies>
</project>

5. Create the Book class

Using the editor, create the Book class with the following content:

datastore-example/src/main/java/com/example/demo/Book.java

package com.example.demo;

import com.google.cloud.spring.data.datastore.core.mapping.Entity;
import org.springframework.data.annotation.Id;

@Entity(name = "books")
public class Book {
  @Id
  Long id;

  String title;

  String author;

  int year;

  public Book(String title, String author, int year) {
    this.title = title;
    this.author = author;
    this.year = year;
  }

  public long getId() {
    return this.id;
  }

  @Override
  public String toString() {
    return "Book{" +
        "id=" + this.id +
        ", title='" + this.title + '\'' +
        ", author='" + this.author + '\'' +
        ", year=" + this.year +
        '}';
  }
}

As you can see, this is a simple POJO. The class is annotated with @Entity to indicate that it can be stored in Datastore and provide the kind name (think of a kind as a table in SQL databases, see documentation for more details). The kind name is optional - if it's omitted, the kind name will be generated based on the class name.

Note that we annotated id property with @Id. That indicates that we want this field to be used as the identifier part of the Datastore Key. Every Datastore entity needs an identifier. Supported types are String and Long.

We override the toString method to make the string representation of the objects more readable; this will be useful when we print them out.

6. Create the BookRepository interface

Create the BookRepository class with the following content:

datastore-example/src/main/java/com/example/demo/BookRepository.java

package com.example.demo;

import java.util.List;

import com.google.cloud.spring.data.datastore.repository.DatastoreRepository;


public interface BookRepository extends DatastoreRepository<Book, Long> {

  List<Book> findByAuthor(String author);

  List<Book> findByYearGreaterThan(int year);

  List<Book> findByAuthorAndYear(String author, int year);
}

The interface extends DatastoreRepository<Book, Long> where Book is the domain class and Long is the Id type. We declare three query methods in our repository for which implementations are generated automatically behind the scenes.

The first one is findByAuthor. As you can guess, the implementation of this method will execute a query that will use a user-provided value in the condition filter for equality to author field.

findByYearGreaterThan method executes a query that filters for the year field greater than user provided value.

findByAuthorAndYear executes a query that looks for entities where author and year fields match to user provided values.

7. Create the interactive CLI application

Open the main application DemoApplication class and modify it to look like this:

datastore-example/src/main/java/com/example/demo/DemoApplication.java

package com.example.demo;

import java.util.List;

import com.google.common.collect.Lists;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;

@ShellComponent
@SpringBootApplication
public class DemoApplication {
  @Autowired
  BookRepository bookRepository;

  public static void main(String[] args) {
     SpringApplication.run(DemoApplication.class, args);
  }

  @ShellMethod("Saves a book to Cloud Datastore: save-book <title> <author> <year>")
  public String saveBook(String title, String author, int year) {
     Book savedBook = this.bookRepository.save(new Book(title, author, year));
     return savedBook.toString();
  }

  @ShellMethod("Loads all books")
  public String findAllBooks() {
     Iterable<Book> books = this.bookRepository.findAll();
     return Lists.newArrayList(books).toString();
  }

  @ShellMethod("Loads books by author: find-by-author <author>")
  public String findByAuthor(String author) {
     List<Book> books = this.bookRepository.findByAuthor(author);
     return books.toString();
  }

  @ShellMethod("Loads books published after a given year: find-by-year-after <year>")
  public String findByYearAfter(int year) {
     List<Book> books = this.bookRepository.findByYearGreaterThan(year);
     return books.toString();
  }

  @ShellMethod("Loads books by author and year: find-by-author-year <author> <year>")
  public String findByAuthorYear(String author, int year) {
     List<Book> books = this.bookRepository.findByAuthorAndYear(author, year);
     return books.toString();
  }

  @ShellMethod("Removes all books")
  public void removeAllBooks() {
     this.bookRepository.deleteAll();
  }
}

Note how we annotated the class with @ShellComponent. That informs Spring that we want to use this class as a source for CLI commands. The methods annotated with @ShellMethod will be exposed as CLI commands in our application.

Here we use the methods we declared in the BookRepository interface: findByAuthor, findByYearGreaterThan, findByAuthorAndYear. Also we use three built-in methods: save, findAll and deleteAll.

Let's look at the saveBook method. We create a Book object using user-provided values for title, author and year. As you can see, we do not provide an id value, so it will be automatically allocated and assigned to the id field on save. The save method accepts an object of type Book and saves it to Cloud Datastore. It returns a Book object with all fields populated, including the id field. In the end we return a string representation of this object.

The rest of the methods work similarly: they accept passed in parameters to the appropriate repository methods and return stringified results.

8. Run the application

To build and start the application, first, make sure JAVA_HOME is set to the right version:

$ export JAVA_HOME=/usr/lib/jvm/java-1.17.0-openjdk-amd64

Execute this command in Cloud Shell (from the root of the project datastore-example/ where the pom.xml is located):

$ ./mvnw spring-boot:run
export JAVA_HOME=/usr/lib/jvm/java-1.17.0-openjdk-amd64

After a successful build stage, the spring logo will show up and the shell prompt will appear:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.0.5)



shell:>

Now you can experiment with the commands we defined earlier. To see the list of commands, use the help command:

shell:> help
...
find-all-books: Loads all books
find-by-author: Loads books by author: find-by-author <author>
find-by-author-year: Loads books by author and year: find-by-author-year <author> <year>
find-by-year-after: Loads books published after a given year: find-by-year-after <year>
remove-all-books: Removes all books
save-book: Saves a book to Cloud Datastore: save-book <title> <author> <year>

Try the following:

  1. Create a few books using save-book command
  2. Run a search using find-all-books command
  3. Find books by specific author: find-by-author <author>
  4. Find books published after specific year: find-by-year-after <year>
  5. Find books by specific author and year: find-by-author-year <author> <year>

9. See what is stored in Datastore using web interface

To see how the entities are stored in Cloud Datastore, go to GCP Console. Input "books" in the kind field, if necessary.

5fab21a6c89f45a.png

10. Clean up

To clean up, remove all books using the aptly named remove-all-books command from the application shell.

shell:> remove-all-books

To exit the application use the quit command, then Ctrl+C.

11. Congratulations!

In this codelab, you've created an interactive CLI application that can store and retrieve objects from Cloud Datastore!

Learn More

License

This work is licensed under a Creative Commons Attribution 2.0 Generic License.