-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: add sample for Spring Data MyBatis
Adds a sample application for using Spring Data and MyBatis with the Cloud Spanner JDBC driver and a PostgreSQL-dialect database.
- Loading branch information
Showing
27 changed files
with
2,495 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
# Spring Data MyBatis Sample Application with Cloud Spanner PostgreSQL | ||
|
||
This sample application shows how to develop portable applications using Spring Data MyBatis 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 MyBatis 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 MyBatis 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. | ||
|
||
## MyBatis Spring | ||
[MyBatis Spring](http://mybatis.org/spring/) integrates MyBatis with the popular Java Spring | ||
framework. This allows MyBatis to participate in Spring transactions and to automatically inject | ||
MyBatis mappers into other beans. | ||
|
||
## Sample Application | ||
|
||
This sample shows how to create a portable application using Spring Data MyBatis 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): | ||
This utility class is used to determine whether the application is running on Cloud Spanner | ||
PostgreSQL or open-source PostgreSQL. This can be used if you have specific features that should | ||
only be executed on one of the two systems. | ||
* [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. | ||
* [SingerService](src/main/java/com/google/cloud/spanner/sample/service/SingerService.java) and | ||
[AlbumService](src/main/java/com/google/cloud/spanner/sample/service/SingerService.java) are | ||
standard Spring service beans that contain business logic that can be executed as transactions. | ||
This includes both read/write and read-only transactions. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
<?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-mybatis-example</artifactId> | ||
<version>1.0-SNAPSHOT</version> | ||
<description> | ||
Sample application showing how to use Spring Data MyBatis with Cloud Spanner PostgreSQL. | ||
</description> | ||
<parent> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter-parent</artifactId> | ||
<version>3.1.3</version> | ||
<relativePath/> | ||
</parent> | ||
<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.mybatis.spring.boot</groupId> | ||
<artifactId>mybatis-spring-boot-starter</artifactId> | ||
<version>3.0.2</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.mybatis.dynamic-sql</groupId> | ||
<artifactId>mybatis-dynamic-sql</artifactId> | ||
<version>1.5.0</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.1</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> |
133 changes: 133 additions & 0 deletions
133
samples/spring-data-mybatis/src/main/java/com/google/cloud/spanner/sample/Application.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
/* | ||
* 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.mappers.AlbumMapper; | ||
import com.google.cloud.spanner.sample.mappers.SingerMapper; | ||
import com.google.cloud.spanner.sample.service.AlbumService; | ||
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 AlbumService albumService; | ||
|
||
private final SingerMapper singerMapper; | ||
|
||
private final AlbumMapper albumMapper; | ||
|
||
public Application( | ||
SingerService singerService, | ||
AlbumService albumService, | ||
DatabaseSeeder databaseSeeder, | ||
SingerMapper singerMapper, | ||
AlbumMapper albumMapper) { | ||
this.databaseSeeder = databaseSeeder; | ||
this.singerService = singerService; | ||
this.albumService = albumService; | ||
this.singerMapper = singerMapper; | ||
this.albumMapper = albumMapper; | ||
} | ||
|
||
@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 = singerMapper.findAll(); | ||
for (Singer singer : allSingers) { | ||
logger.info( | ||
"Found singer: {} with {} albums", | ||
singer, | ||
albumMapper.countAlbumsBySingerId(singer.getId())); | ||
for (Album album : albumMapper.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 Album and some Tracks in a read/write transaction. | ||
// Track is an interleaved table. | ||
Album album = new Album(DatabaseSeeder.randomTitle()); | ||
album.setSingerId(insertedSinger.getId()); | ||
albumService.createAlbumAndTracks( | ||
album, | ||
new Track(album, 1, DatabaseSeeder.randomTitle(), 3.14d), | ||
new Track(album, 2, DatabaseSeeder.randomTitle(), 3.14d), | ||
new Track(album, 3, DatabaseSeeder.randomTitle(), 3.14d), | ||
new Track(album, 4, DatabaseSeeder.randomTitle(), 3.14d), | ||
new Track(album, 5, DatabaseSeeder.randomTitle(), 3.14d), | ||
new Track(album, 6, DatabaseSeeder.randomTitle(), 3.14d), | ||
new Track(album, 7, DatabaseSeeder.randomTitle(), 3.14d)); | ||
logger.info("Inserted album {}", album.getTitle()); | ||
|
||
// 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 : singerMapper.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()); | ||
} | ||
} | ||
} |
Oops, something went wrong.