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
- Prerequisites
- Project Setup
- GCP Datastore Configuration
- Entity Modeling
- Repository Layer
- Service Layer
- REST Controller
- Testing
- Postman Collection
- 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:
- Go to "IAM & Admin" > "Service Accounts"
- Create a new service account with "Datastore User" role
- 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
- Containerization:
- Create a Dockerfile for your application
- Consider using Jib for building optimized container images
- Cloud Run:
- Ideal for stateless applications
- Automatic scaling
- Pay-per-use pricing
- App Engine:
- Fully managed environment
- Good for applications with variable traffic
- Kubernetes Engine:
- For complex microservices architectures
- More control over infrastructure
- Configuration:
- Use Secret Manager for sensitive credentials
- Consider using Spring Cloud Config with GCP Runtime Config
- Monitoring:
- Integrate with Cloud Monitoring and Logging
- Use Cloud Trace for distributed tracing