CODE WITH SIBIN

Solving Real Problems with Real Code


Google Cloud Datastore + Spring Boot 3: The Ultimate Guide

This guide provides a comprehensive walkthrough for integrating Google Cloud Datastore with Spring Boot, including setup, entity modeling, repository, service layer, REST controller, testing, and Postman examples.

Table of Contents

  1. Prerequisites
  2. Project Setup
  3. GCP Datastore Configuration
  4. Entity Modeling
  5. Repository Layer
  6. Service Layer
  7. REST Controller
  8. Testing
  9. Postman Collection
  10. Deployment Considerations

Prerequisites

  • Java 17 or higher
  • Maven 3.6+
  • Google Cloud account
  • GCP project with Datastore enabled
  • GCP credentials configured locally

Project Setup

Updated pom.xml for Spring Boot 3.0.5

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.5</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>gcp-datastore-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gcp-datastore-demo</name>
    <description>Demo project for Spring Boot with GCP Datastore</description>

    <properties>
        <java.version>17</java.version>
        <spring-cloud-gcp.version>4.1.0</spring-cloud-gcp.version>
        <spring-cloud.version>2022.0.1</spring-cloud.version>
    </properties>

    <dependencies>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

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

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- Jakarta Persistence API (for @Id) -->
        <dependency>
            <groupId>jakarta.persistence</groupId>
            <artifactId>jakarta.persistence-api</artifactId>
            <version>3.1.0</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.google.cloud</groupId>
                <artifactId>spring-cloud-gcp-dependencies</artifactId>
                <version>${spring-cloud-gcp.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

GCP Datastore Configuration

1. Enable Datastore API

  • Go to Google Cloud Console
  • Navigate to "APIs & Services" > "Library"
  • Search for "Cloud Datastore API" and enable it

2. Set Up Authentication

Create a service account and download the JSON key file:

  1. Go to "IAM & Admin" > "Service Accounts"
  2. Create a new service account with "Datastore User" role
  3. Generate and download the JSON key file

3. Configure application.properties

# Application
server.port=8080

# GCP Configuration
spring.cloud.gcp.project-id=your-project-id
spring.cloud.gcp.credentials.location=file:/path/to/your/service-account-key.json

# Datastore
spring.cloud.gcp.datastore.namespace=demo-namespace
spring.cloud.gcp.datastore.emulator.enabled=false

For local development with emulator:

spring.cloud.gcp.datastore.emulator.enabled=true
spring.cloud.gcp.datastore.emulator.port=8081

Entity Modeling

Updated for Jakarta Persistence annotations:

package com.example.gcpdatastoredemo.model;

import com.google.cloud.spring.data.datastore.core.mapping.Entity;
import jakarta.persistence.Id;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity(name = "customers")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Customer {
    @Id
    private Long id;

    @NotBlank(message = "First name is required")
    @Size(max = 50, message = "First name must be less than 50 characters")
    private String firstName;

    @NotBlank(message = "Last name is required")
    @Size(max = 50, message = "Last name must be less than 50 characters")
    private String lastName;

    @NotBlank(message = "Email is required")
    @Email(message = "Email should be valid")
    private String email;

    private int age;
    private boolean active;
}

Repository Layer

package com.example.gcpdatastoredemo.repository;

import com.example.gcpdatastoredemo.model.Customer;
import com.google.cloud.spring.data.datastore.repository.DatastoreRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface CustomerRepository extends DatastoreRepository<Customer, Long> {
    List<Customer> findByLastName(String lastName);
    List<Customer> findByEmail(String email);
    List<Customer> findByActive(boolean active);
}

Service Layer

package com.example.gcpdatastoredemo.service;

import com.example.gcpdatastoredemo.model.Customer;
import com.example.gcpdatastoredemo.repository.CustomerRepository;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class CustomerService {

    private final CustomerRepository customerRepository;

    public CustomerService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    public List<Customer> getAllCustomers() {
        return (List<Customer>) customerRepository.findAll();
    }

    public Optional<Customer> getCustomerById(Long id) {
        return customerRepository.findById(id);
    }

    public Customer createCustomer(Customer customer) {
        return customerRepository.save(customer);
    }

    public Customer updateCustomer(Long id, Customer customerDetails) {
        return customerRepository.findById(id)
                .map(customer -> {
                    customer.setFirstName(customerDetails.getFirstName());
                    customer.setLastName(customerDetails.getLastName());
                    customer.setEmail(customerDetails.getEmail());
                    customer.setAge(customerDetails.getAge());
                    customer.setActive(customerDetails.isActive());
                    return customerRepository.save(customer);
                })
                .orElseGet(() -> {
                    customerDetails.setId(id);
                    return customerRepository.save(customerDetails);
                });
    }

    public void deleteCustomer(Long id) {
        customerRepository.deleteById(id);
    }

    public List<Customer> getActiveCustomers() {
        return customerRepository.findByActive(true);
    }

    public List<Customer> getCustomersByLastName(String lastName) {
        return customerRepository.findByLastName(lastName);
    }
}

REST Controller

package com.example.gcpdatastoredemo.controller;

import com.example.gcpdatastoredemo.model.Customer;
import com.example.gcpdatastoredemo.service.CustomerService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/customers")
public class CustomerController {

    private final CustomerService customerService;

    public CustomerController(CustomerService customerService) {
        this.customerService = customerService;
    }

    @GetMapping
    public ResponseEntity<List<Customer>> getAllCustomers() {
        return ResponseEntity.ok(customerService.getAllCustomers());
    }

    @GetMapping("/{id}")
    public ResponseEntity<Customer> getCustomerById(@PathVariable Long id) {
        Optional<Customer> customer = customerService.getCustomerById(id);
        return customer.map(ResponseEntity::ok)
                .orElseGet(() -> ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<Customer> createCustomer(@Valid @RequestBody Customer customer) {
        Customer savedCustomer = customerService.createCustomer(customer);
        return ResponseEntity.status(HttpStatus.CREATED).body(savedCustomer);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Customer> updateCustomer(
            @PathVariable Long id, @Valid @RequestBody Customer customerDetails) {
        Customer updatedCustomer = customerService.updateCustomer(id, customerDetails);
        return ResponseEntity.ok(updatedCustomer);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteCustomer(@PathVariable Long id) {
        customerService.deleteCustomer(id);
        return ResponseEntity.noContent().build();
    }

    @GetMapping("/active")
    public ResponseEntity<List<Customer>> getActiveCustomers() {
        return ResponseEntity.ok(customerService.getActiveCustomers());
    }

    @GetMapping("/search")
    public ResponseEntity<List<Customer>> getCustomersByLastName(
            @RequestParam String lastName) {
        return ResponseEntity.ok(customerService.getCustomersByLastName(lastName));
    }
}

Testing

Unit Tests

package com.example.gcpdatastoredemo.service;

import com.example.gcpdatastoredemo.model.Customer;
import com.example.gcpdatastoredemo.repository.CustomerRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class CustomerServiceTest {

    @Mock
    private CustomerRepository customerRepository;

    @InjectMocks
    private CustomerService customerService;

    @Test
    void getAllCustomers() {
        // Arrange
        Customer customer1 = new Customer(1L, "John", "Doe", "john@example.com", 30, true);
        Customer customer2 = new Customer(2L, "Jane", "Smith", "jane@example.com", 25, true);
        when(customerRepository.findAll()).thenReturn(Arrays.asList(customer1, customer2));

        // Act
        List<Customer> customers = customerService.getAllCustomers();

        // Assert
        assertEquals(2, customers.size());
        verify(customerRepository, times(1)).findAll();
    }

    @Test
    void getCustomerById() {
        // Arrange
        Long customerId = 1L;
        Customer customer = new Customer(customerId, "John", "Doe", "john@example.com", 30, true);
        when(customerRepository.findById(customerId)).thenReturn(Optional.of(customer));

        // Act
        Optional<Customer> foundCustomer = customerService.getCustomerById(customerId);

        // Assert
        assertTrue(foundCustomer.isPresent());
        assertEquals(customerId, foundCustomer.get().getId());
        verify(customerRepository, times(1)).findById(customerId);
    }

    @Test
    void createCustomer() {
        // Arrange
        Customer newCustomer = new Customer(null, "New", "Customer", "new@example.com", 40, true);
        Customer savedCustomer = new Customer(1L, "New", "Customer", "new@example.com", 40, true);
        when(customerRepository.save(newCustomer)).thenReturn(savedCustomer);

        // Act
        Customer result = customerService.createCustomer(newCustomer);

        // Assert
        assertNotNull(result.getId());
        assertEquals(savedCustomer.getEmail(), result.getEmail());
        verify(customerRepository, times(1)).save(newCustomer);
    }
}

Integration Tests

package com.example.gcpdatastoredemo;

import com.example.gcpdatastoredemo.model.Customer;
import com.example.gcpdatastoredemo.repository.CustomerRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CustomerControllerIntegrationTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private CustomerRepository customerRepository;

    @AfterEach
    void tearDown() {
        customerRepository.deleteAll();
    }

    @Test
    void createCustomer() {
        // Arrange
        String url = "http://localhost:" + port + "/api/customers";
        Customer customer = new Customer(null, "Integration", "Test", "integration@test.com", 35, true);

        // Act
        ResponseEntity<Customer> response = restTemplate.postForEntity(url, customer, Customer.class);

        // Assert
        assertEquals(HttpStatus.CREATED, response.getStatusCode());
        assertNotNull(response.getBody().getId());
        assertEquals("Integration", response.getBody().getFirstName());
    }

    @Test
    void getCustomerById() {
        // Arrange
        Customer savedCustomer = customerRepository.save(
                new Customer(null, "Get", "Customer", "get@customer.com", 40, true));
        String url = "http://localhost:" + port + "/api/customers/" + savedCustomer.getId();

        // Act
        ResponseEntity<Customer> response = restTemplate.getForEntity(url, Customer.class);

        // Assert
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertEquals(savedCustomer.getId(), response.getBody().getId());
        assertEquals("Get", response.getBody().getFirstName());
    }
}

Postman Collection

Here's a Postman collection you can import:

{
  "info": {
    "_postman_id": "a1b2c3d4-e5f6-7890",
    "name": "GCP Datastore Demo",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "Create Customer",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"firstName\": \"John\",\n  \"lastName\": \"Doe\",\n  \"email\": \"john.doe@example.com\",\n  \"age\": 30,\n  \"active\": true\n}"
        },
        "url": {
          "raw": "{{baseUrl}}/api/customers",
          "host": ["{{baseUrl}}"],
          "path": ["api","customers"]
        }
      },
      "response": []
    },
    {
      "name": "Get All Customers",
      "request": {
        "method": "GET",
        "header": [],
        "url": {
          "raw": "{{baseUrl}}/api/customers",
          "host": ["{{baseUrl}}"],
          "path": ["api","customers"]
        }
      },
      "response": []
    },
    {
      "name": "Get Customer by ID",
      "request": {
        "method": "GET",
        "header": [],
        "url": {
          "raw": "{{baseUrl}}/api/customers/1",
          "host": ["{{baseUrl}}"],
          "path": ["api","customers","1"]
        }
      },
      "response": []
    },
    {
      "name": "Update Customer",
      "request": {
        "method": "PUT",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"firstName\": \"John\",\n  \"lastName\": \"Doe\",\n  \"email\": \"john.doe.updated@example.com\",\n  \"age\": 31,\n  \"active\": true\n}"
        },
        "url": {
          "raw": "{{baseUrl}}/api/customers/1",
          "host": ["{{baseUrl}}"],
          "path": ["api","customers","1"]
        }
      },
      "response": []
    },
    {
      "name": "Delete Customer",
      "request": {
        "method": "DELETE",
        "header": [],
        "url": {
          "raw": "{{baseUrl}}/api/customers/1",
          "host": ["{{baseUrl}}"],
          "path": ["api","customers","1"]
        }
      },
      "response": []
    },
    {
      "name": "Get Active Customers",
      "request": {
        "method": "GET",
        "header": [],
        "url": {
          "raw": "{{baseUrl}}/api/customers/active",
          "host": ["{{baseUrl}}"],
          "path": ["api","customers","active"]
        }
      },
      "response": []
    },
    {
      "name": "Search Customers by Last Name",
      "request": {
        "method": "GET",
        "header": [],
        "url": {
          "raw": "{{baseUrl}}/api/customers/search?lastName=Doe",
          "host": ["{{baseUrl}}"],
          "path": ["api","customers","search"],
          "query": [
            {
              "key": "lastName",
              "value": "Doe"
            }
          ]
        }
      },
      "response": []
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "http://localhost:8080"
    }
  ]
}

Deployment Considerations

  1. Containerization:
    • Create a Dockerfile for your application
    • Consider using Jib for building optimized container images
  2. Cloud Run:
    • Ideal for stateless applications
    • Automatic scaling
    • Pay-per-use pricing
  3. App Engine:
    • Fully managed environment
    • Good for applications with variable traffic
  4. Kubernetes Engine:
    • For complex microservices architectures
    • More control over infrastructure
  5. Configuration:
    • Use Secret Manager for sensitive credentials
    • Consider using Spring Cloud Config with GCP Runtime Config
  6. Monitoring:
    • Integrate with Cloud Monitoring and Logging
    • Use Cloud Trace for distributed tracing

Leave a Reply

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