Skip to content

Commit

Permalink
docs: run MyBatis sample on the Emulator and add insert-or-update sam…
Browse files Browse the repository at this point in the history
…ple (#1737)

* docs: run MyBatis sample on the Emulator

Adds an option to the MyBatis sample to also run on the Spanner Emulator,
and add a test run that uses this option.

* chore: remove commented code

* docs: add insert-or-update example

* fix: set spanner.emulator property in test

* fix: update copy-paste error
  • Loading branch information
olavloite authored Sep 12, 2024
1 parent 56c5730 commit 02534d7
Show file tree
Hide file tree
Showing 12 changed files with 323 additions and 43 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/spring-data-mybatis-sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ on:
pull_request:
name: spring-data-mybatis-sample
jobs:
spring-data-jdbc:
spring-data-mybatis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
15 changes: 11 additions & 4 deletions samples/spring-data-mybatis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This sample shows:
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.
4. How to use the Spanner Emulator for development in combination with Spring Data.

__NOTE__: This application does __not require PGAdapter__. Instead, it connects to Cloud Spanner
PostgreSQL using the Cloud Spanner JDBC driver.
Expand Down Expand Up @@ -58,13 +59,16 @@ The default profile is `cs`. You can change the default profile by modifying the
### 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
`cs`, which runs the application on Cloud Spanner PostgreSQL.
2. The sample by default starts an instance of the Spanner Emulator together with the application and
runs the application against the emulator.
3. Modify the default profile in the [application.properties](src/main/resources/application.properties)
file to run the sample on an open-source PostgreSQL database.
4. 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`.
5. Run the application with `mvn spring-boot:run`.

### Main Application Components

Expand All @@ -79,6 +83,9 @@ The main application components are:
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.
* [EmulatorInitializer.java](src/main/java/com/google/cloud/spanner/sample/EmulatorInitializer.java):
This ApplicationListener automatically starts the Spanner emulator as a Docker container if the
sample has been configured to run on the emulator.
* [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
Expand Down
12 changes: 11 additions & 1 deletion samples/spring-data-mybatis/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>1.20.1</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down Expand Up @@ -70,6 +77,10 @@
<artifactId>postgresql</artifactId>
<version>42.7.4</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
</dependency>

<dependency>
<groupId>com.google.collections</groupId>
Expand All @@ -94,7 +105,6 @@
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@

package com.google.cloud.spanner.sample;

import com.google.cloud.spanner.connection.SpannerPool;
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 java.util.concurrent.ThreadLocalRandom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
Expand All @@ -34,7 +36,15 @@ 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();
EmulatorInitializer emulatorInitializer = new EmulatorInitializer();
try {
SpringApplication application = new SpringApplication(Application.class);
application.addListeners(emulatorInitializer);
application.run(args).close();
} finally {
SpannerPool.closeSpannerPool();
emulatorInitializer.stopEmulator();
}
}

private final DatabaseSeeder databaseSeeder;
Expand Down Expand Up @@ -129,5 +139,22 @@ public void run(String... args) {
for (Singer singer : singerService.listSingersWithLastNameStartingWith("A", "B", "C")) {
logger.info("\t{}", singer.getFullName());
}

// Execute an insert-or-update for a Singer record.
// For this, we either get a random Singer from the database, or create a new Singer entity
// and assign it a random ID.
logger.info("Executing an insert-or-update statement for a Singer record");
Singer singer;
if (ThreadLocalRandom.current().nextBoolean()) {
singer = singerMapper.getRandom();
} else {
singer = new Singer();
singer.setId(ThreadLocalRandom.current().nextLong());
}
singer.setFirstName("Beatriz");
singer.setLastName("Russel");
singer.setActive(true);
// This executes an INSERT ... ON CONFLICT DO UPDATE statement.
singerMapper.insertOrUpdate(singer);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2024 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 org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.images.PullPolicy;
import org.testcontainers.utility.DockerImageName;

public class EmulatorInitializer
implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
private GenericContainer<?> emulator;

@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
ConfigurableEnvironment environment = event.getEnvironment();
boolean useEmulator =
Boolean.TRUE.equals(environment.getProperty("spanner.emulator", Boolean.class));
if (!useEmulator) {
return;
}

emulator =
new GenericContainer<>(DockerImageName.parse("gcr.io/cloud-spanner-emulator/emulator"));
emulator.withImagePullPolicy(PullPolicy.alwaysPull());
emulator.addExposedPort(9010);
emulator.setWaitStrategy(Wait.forListeningPorts(9010));
emulator.start();

System.setProperty("spanner.endpoint", "//localhost:" + emulator.getMappedPort(9010));
}

public void stopEmulator() {
if (this.emulator != null) {
this.emulator.stop();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public interface SingerMapper {
@Select("SELECT * FROM singers WHERE id = #{singerId}")
Singer get(@Param("singerId") long singerId);

@Select("SELECT * FROM singers ORDER BY sha256(last_name::bytea) LIMIT 1")
Singer getRandom();

@Select("SELECT * FROM singers ORDER BY last_name, first_name, id")
List<Singer> findAll();

Expand All @@ -47,6 +50,24 @@ public interface SingerMapper {
@Options(useGeneratedKeys = true, keyProperty = "id,fullName")
int insert(Singer singer);

/**
* Executes an insert-or-update statement for a Singer record. Note that the id must have been set
* manually on the Singer entity before calling this method, and that Spanner requires that all
* columns for the INSERT statement must also be included in the UPDATE statement, including the
* 'id' column. The statement only returns the 'fullName' property, because the 'id' is already
* known.
*/
@Insert(
"INSERT INTO singers (id, first_name, last_name, active) "
+ "VALUES (#{id}, #{firstName}, #{lastName}, #{active}) "
+ "ON CONFLICT (id) DO UPDATE SET "
+ "id=excluded.id, "
+ "first_name=excluded.first_name, "
+ "last_name=excluded.last_name, "
+ "active=excluded.active")
@Options(useGeneratedKeys = true, keyProperty = "fullName")
int insertOrUpdate(Singer singer);

/** Updates an existing singer and returns the generated full name. */
@Update(
"UPDATE singers SET "
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@

# This profile uses a Cloud Spanner PostgreSQL database.

# The sample by default uses the Spanner emulator.
# Disable this flag to run the sample on a real Spanner instance.
spanner.emulator=true
# This property is automatically set to point to the Spanner emulator that is automatically
# started together with the application. It remains empty if the application is executed
# against a real Spanner instance.
spanner.endpoint=
# Used for testing
spanner.additional_properties=

# Update these properties to match your project, instance, and database.
spanner.project=my-project
spanner.instance=my-instance
spanner.database=mybatis-sample

spring.datasource.url=jdbc:cloudspanner:/projects/${spanner.project}/instances/${spanner.instance}/databases/${spanner.database}
spring.datasource.url=jdbc:cloudspanner:${spanner.endpoint}/projects/${spanner.project}/instances/${spanner.instance}/databases/${spanner.database};dialect=POSTGRESQL;autoConfigEmulator=${spanner.emulator};${spanner.additional_properties}
spring.datasource.driver-class-name=com.google.cloud.spanner.jdbc.JdbcDriver
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2024 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 org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class ApplicationEmulatorTest {

@Test
public void testRunApplicationOnEmulator() {
System.setProperty("spanner.emulator", "true");
Application.main(new String[] {});
}
}
Loading

0 comments on commit 02534d7

Please sign in to comment.