Skip to content

Commit

Permalink
docs: create Spring Data JDBC sample (#1334)
Browse files Browse the repository at this point in the history
Adds a sample for using Spring Data JDBC with Cloud Spanner PostgreSQL and shows how to run the same application on both Cloud Spanner and traditional PostgreSQL. This can be used to create a portable application, or to use PostgreSQL as a development/test database, while Cloud Spanner is used for production.
  • Loading branch information
olavloite committed Sep 14, 2023
1 parent ff6648f commit cefea55
Show file tree
Hide file tree
Showing 27 changed files with 2,324 additions and 0 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/spring-data-jdbc-sample.yaml
@@ -0,0 +1,30 @@
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Github action job to test core java library features on
# downstream client libraries before they are released.
on:
pull_request:
name: spring-data-jdbc-sample
jobs:
spring-data-jdbc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
- name: Run tests
run: mvn test
working-directory: samples/spring-data-jdbc
95 changes: 95 additions & 0 deletions samples/spring-data-jdbc/README.md
@@ -0,0 +1,95 @@
# Spring Data JDBC Sample Application with Cloud Spanner PostgreSQL

This sample application shows how to develop portable applications using Spring Data JDBC in
combination with Cloud Spanner PostgreSQL. This application can be configured to run on either a
[Cloud Spanner PostgreSQL](https://cloud.google.com/spanner/docs/postgresql-interface) database or
an open-source PostgreSQL database. The only change that is needed to switch between the two is
changing the active Spring profile that is used by the application.

The application uses the Cloud Spanner JDBC driver to connect to Cloud Spanner PostgreSQL, and it
uses the PostgreSQL JDBC driver to connect to open-source PostgreSQL. Spring Data JDBC works with
both drivers and offers a single consistent API to the application developer, regardless of the
actual database or JDBC driver being used.

This sample shows:

1. How to use Spring Data JDBC with Cloud Spanner PostgreSQL.
2. How to develop a portable application that runs on both Google Cloud Spanner PostgreSQL and
open-source PostgreSQL with the same code base.
3. How to use bit-reversed sequences to automatically generate primary key values for entities.

__NOTE__: This application does __not require PGAdapter__. Instead, it connects to Cloud Spanner
PostgreSQL using the Cloud Spanner JDBC driver.

## Cloud Spanner PostgreSQL

Cloud Spanner PostgreSQL provides language support by expressing Spanner database functionality
through a subset of open-source PostgreSQL language constructs, with extensions added to support
Spanner functionality like interleaved tables and hinting.

The PostgreSQL interface makes the capabilities of Spanner —__fully managed, unlimited scale, strong
consistency, high performance, and up to 99.999% global availability__— accessible using the
PostgreSQL dialect. Unlike other services that manage actual PostgreSQL database instances, Spanner
uses PostgreSQL-compatible syntax to expose its existing scale-out capabilities. This provides
familiarity for developers and portability for applications, but not 100% PostgreSQL compatibility.
The SQL syntax that Spanner supports is semantically equivalent PostgreSQL, meaning schemas
and queries written against the PostgreSQL interface can be easily ported to another PostgreSQL
environment.

This sample showcases this portability with an application that works on both Cloud Spanner PostgreSQL
and open-source PostgreSQL with the same code base.

## Spring Data JDBC

[Spring Data JDBC](https://spring.io/projects/spring-data-jdbc) is part of the larger Spring Data
family. It makes it easy to implement JDBC based repositories. This module deals with enhanced
support for JDBC based data access layers.

Spring Data JDBC aims at being conceptually easy. In order to achieve this it does NOT offer caching,
lazy loading, write behind or many other features of JPA. This makes Spring Data JDBC a simple,
limited, opinionated ORM.

## Sample Application

This sample shows how to create a portable application using Spring Data JDBC and the Cloud Spanner
PostgreSQL dialect. The application works on both Cloud Spanner PostgreSQL and open-source
PostgreSQL. You can switch between the two by changing the active Spring profile:
* Profile `cs` runs the application on Cloud Spanner PostgreSQL.
* Profile `pg` runs the application on open-source PostgreSQL.

The default profile is `cs`. You can change the default profile by modifying the
[application.properties](src/main/resources/application.properties) file.

### Running the Application

1. Choose the database system that you want to use by choosing a profile. The default profile is
`cs`, which runs the application on Cloud Spanner PostgreSQL. Modify the default profile in the
[application.properties](src/main/resources/application.properties) file.
2. Modify either [application-cs.properties](src/main/resources/application-cs.properties) or
[application-pg.properties](src/main/resources/application-pg.properties) to point to an existing
database. If you use Cloud Spanner, the database that the configuration file references must be a
database that uses the PostgreSQL dialect.
3. Run the application with `mvn spring-boot:run`.

### Main Application Components

The main application components are:
* [DatabaseSeeder.java](src/main/java/com/google/cloud/spanner/sample/DatabaseSeeder.java): This
class is responsible for creating the database schema and inserting some initial test data. The
schema is created from the [create_schema.sql](src/main/resources/create_schema.sql) file. The
`DatabaseSeeder` class loads this file into memory and executes it on the active database using
standard JDBC APIs. The class also removes Cloud Spanner-specific extensions to the PostgreSQL
dialect when the application runs on open-source PostgreSQL.
* [JdbcConfiguration.java](src/main/java/com/google/cloud/spanner/sample/JdbcConfiguration.java):
Spring Data JDBC by default detects the database dialect based on the JDBC driver that is used.
This class overrides this default and instructs Spring Data JDBC to also use the PostgreSQL
dialect for Cloud Spanner PostgreSQL.
* [AbstractEntity.java](src/main/java/com/google/cloud/spanner/sample/entities/AbstractEntity.java):
This is the shared base class for all entities in this sample application. It defines a number of
standard attributes, such as the identifier (primary key). The primary key is automatically
generated using a (bit-reversed) sequence. [Bit-reversed sequential values](https://cloud.google.com/spanner/docs/schema-design#bit_reverse_primary_key)
are considered a good choice for primary keys on Cloud Spanner.
* [Application.java](src/main/java/com/google/cloud/spanner/sample/Application.java): The starter
class of the application. It contains a command-line runner that executes a selection of queries
and updates on the database.

102 changes: 102 additions & 0 deletions samples/spring-data-jdbc/pom.xml
@@ -0,0 +1,102 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>cloud-spanner-spring-data-jdbc-example</artifactId>
<version>1.0-SNAPSHOT</version>
<description>
Sample application showing how to use Spring Data JDBC with Cloud Spanner PostgreSQL.
</description>

<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-bom</artifactId>
<version>2023.0.3</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
<version>26.22.0</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
<version>3.1.3</version>
</dependency>

<!-- Add both the Cloud Spanner and the PostgreSQL JDBC driver. -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-spanner-jdbc</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.6.0</version>
</dependency>

<dependency>
<groupId>com.google.collections</groupId>
<artifactId>google-collections</artifactId>
<version>1.0</version>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-spanner</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.api</groupId>
<artifactId>gax-grpc</artifactId>
<classifier>testlib</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>com.spotify.fmt</groupId>
<artifactId>fmt-maven-plugin</artifactId>
<version>2.20</version>
<executions>
<execution>
<goals>
<goal>format</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,126 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.spanner.sample;

import com.google.cloud.spanner.sample.entities.Album;
import com.google.cloud.spanner.sample.entities.Singer;
import com.google.cloud.spanner.sample.entities.Track;
import com.google.cloud.spanner.sample.repositories.AlbumRepository;
import com.google.cloud.spanner.sample.repositories.SingerRepository;
import com.google.cloud.spanner.sample.repositories.TrackRepository;
import com.google.cloud.spanner.sample.service.SingerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(Application.class);

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

private final DatabaseSeeder databaseSeeder;

private final SingerService singerService;

private final SingerRepository singerRepository;

private final AlbumRepository albumRepository;

private final TrackRepository trackRepository;

public Application(
SingerService singerService,
DatabaseSeeder databaseSeeder,
SingerRepository singerRepository,
AlbumRepository albumRepository,
TrackRepository trackRepository) {
this.databaseSeeder = databaseSeeder;
this.singerService = singerService;
this.singerRepository = singerRepository;
this.albumRepository = albumRepository;
this.trackRepository = trackRepository;
}

@Override
public void run(String... args) {

// Set the system property 'drop_schema' to true to drop any existing database
// schema when the application is executed.
if (Boolean.parseBoolean(System.getProperty("drop_schema", "false"))) {
logger.info("Dropping existing schema if it exists");
databaseSeeder.dropDatabaseSchemaIfExists();
}

logger.info("Creating database schema if it does not already exist");
databaseSeeder.createDatabaseSchemaIfNotExists();
logger.info("Deleting existing test data");
databaseSeeder.deleteTestData();
logger.info("Inserting fresh test data");
databaseSeeder.insertTestData();

Iterable<Singer> allSingers = singerRepository.findAll();
for (Singer singer : allSingers) {
logger.info(
"Found singer: {} with {} albums",
singer,
albumRepository.countAlbumsBySingerId(singer.getId()));
for (Album album : albumRepository.findAlbumsBySingerId(singer.getId())) {
logger.info("\tAlbum: {}, released at {}", album, album.getReleaseDate());
}
}

// Create a new singer and three albums in a transaction.
Singer insertedSinger =
singerService.createSingerAndAlbums(
new Singer("Amethyst", "Jiang"),
new Album(databaseSeeder.randomTitle()),
new Album(databaseSeeder.randomTitle()),
new Album(databaseSeeder.randomTitle()));
logger.info(
"Inserted singer {} {} {}",
insertedSinger.getId(),
insertedSinger.getFirstName(),
insertedSinger.getLastName());

// Create a new track record and insert it into the database.
Album album = albumRepository.getFirst().orElseThrow();
Track track = new Track(album, 1, databaseSeeder.randomTitle());
track.setSampleRate(3.14d);
// Spring Data JDBC supports the same base CRUD operations on entities as for example
// Spring Data JPA.
trackRepository.save(track);

// List all singers that have a last name starting with an 'J'.
logger.info("All singers with a last name starting with an 'J':");
for (Singer singer : singerRepository.findSingersByLastNameStartingWith("J")) {
logger.info("\t{}", singer.getFullName());
}

// The singerService.listSingersWithLastNameStartingWith(..) method uses a read-only
// transaction. You should prefer read-only transactions to read/write transactions whenever
// possible, as read-only transactions do not take locks.
logger.info("All singers with a last name starting with an 'A', 'B', or 'C'.");
for (Singer singer : singerService.listSingersWithLastNameStartingWith("A", "B", "C")) {
logger.info("\t{}", singer.getFullName());
}
}
}

0 comments on commit cefea55

Please sign in to comment.