Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: run Spring Data JPA sample on the Emulator #2245

Merged
merged 2 commits into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/samples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ jobs:
- name: Run Hibernate Sample tests
working-directory: ./samples/java/hibernate
run: mvn test -B -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn
- name: Run Spring Data JPA Sample tests
working-directory: ./samples/java/spring-data-jpa
run: mvn test -B -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn
go-samples:
runs-on: ubuntu-latest
steps:
Expand Down
49 changes: 31 additions & 18 deletions samples/java/spring-data-jpa/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,23 @@ The sample application starts PGAdapter as an in-process dependency, and uses th
PostgreSQL JDBC driver and Hibernate dialect to connect through the in-process PGAdapter to Cloud
Spanner.

## Running
It also shows how to integrate Liquibase into a Spring Boot application.

## Running on the Emulator
The sample by default also starts a Spanner Emulator instance in a Docker test container and runs
the sample on the Emulator. The host machine must have Docker installed for this to work.
No other prior setup is needed to run the sample application on the Spanner Emulator.

Run the application from your favorite IDE or execute it from the command line with:

```shell
mvn spring-boot:run
```

## Running on a real Spanner Database
Modify the `application.properties` file in the [src/main/resources](src/main/resources) directory
to match your Cloud Spanner database. The database must exist and must use the PostgreSQL dialect.
to match your Cloud Spanner database and set the property `spanner.use_emulator=false`.
The database must exist and must use the PostgreSQL dialect.
The application will automatically create the required tables when the application is starting.

Run the application from your favorite IDE or execute it from the command line with:
Expand All @@ -16,8 +30,6 @@ Run the application from your favorite IDE or execute it from the command line w
mvn spring-boot:run
```

See [Troubleshooting](#troubleshooting) if you run into unexpected errors.

## Integration with IntelliJ

It is recommended to [follow these instructions](../../../docs/intellij.md) to add your Cloud
Expand Down Expand Up @@ -53,6 +65,9 @@ development and deployment process, as you only have one application that needs
started. Running PGAdapter and your application in the same JVM will also give you minimal latency
between your application and PGAdaper.

This sample also shows how to automatically start the Spanner Emulator together with PGAdapter, so
you can use the Spanner Emulator for local development.

### UUID Primary Keys

The [AbstractUuidEntity](src/main/java/com/google/cloud/spanner/pgadapter/sample/model/AbstractUuidEntity.java)
Expand Down Expand Up @@ -194,6 +209,18 @@ contains an example of a helper method that can be used to execute stale reads.
The [SampleApplication.java](src/main/java/com/google/cloud/spanner/pgadapter/sample/SampleApplication.java)
contains a `staleRead()` method that shows how to use the `StaleReadService`.

### Directed Reads

Cloud Spanner supports [Directed Reads](https://cloud.google.com/spanner/docs/directed-reads) to
provide flexibility to route read-only transactions to specific regions.

Directed reads are not part of the standard JPA interface. It is however possible to execute
directed reads by executing [session management commands](https://cloud.google.com/spanner/docs/jdbc-session-mgmt-commands-pgcompat).
The [DirectedReadService](src/main/java/com/google/cloud/spanner/pgadapter/sample/service/DirectedReadService.java)
contains an example of a helper method that can be used to execute directed reads.
The [SampleApplication.java](src/main/java/com/google/cloud/spanner/pgadapter/sample/SampleApplication.java)
contains a `directedRead()` method that shows how to use the `DirectedReadService`.

## Liquibase
The sample application uses Liquibase to manage the database schema. It is recommended to use
a higher level schema management system like Liquibase to manage your database schema for multiple
Expand Down Expand Up @@ -223,17 +250,3 @@ mvn liquibase:rollback \
The `spanner.ddl_transaction_mode=AutocommitExplicitTransaction` addition to the above JDBC connection
URL ensures that PGAdapter will automatically commit any active transaction when it encounters a DDL
statement, and then execute all following DDL statements as a single DDL batch.

## Troubleshooting

### Address already in use

The application starts PGAdapter on port `9432` on your local machine. The following error can occur
when you run the application if another process is already using that port number.

```
Server on port 9432 stopped by exception: java.net.BindException: Address already in use
```

You can change the port number that is used for PGAdapter by changing the value in the
[PGAdapter.java](src/main/java/com/google/cloud/spanner/pgadapter/sample/PGAdapter.java) file.
29 changes: 29 additions & 0 deletions samples/java/spring-data-jpa/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@
<properties>
<java.version>17</java.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>1.20.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- Add Spring Boot Data JPA -->
<dependency>
Expand Down Expand Up @@ -66,6 +79,11 @@
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand All @@ -79,6 +97,17 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit47</artifactId>
<version>${maven-surefire-plugin.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>com.spotify.fmt</groupId>
<artifactId>fmt-maven-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,44 +18,80 @@
import com.google.cloud.spanner.pgadapter.ProxyServer;
import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
import com.google.common.base.Strings;
import io.opentelemetry.api.OpenTelemetry;
import java.util.Properties;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;

/** Util class for managing the in-process PGAdapter instance used by thie sample. */
/** Util class for managing the in-process PGAdapter instance used by this sample. */
class PGAdapter {
private final GenericContainer<?> emulator;
private final ProxyServer server;

public PGAdapter() {
this.server = startPGAdapter();
PGAdapter(boolean startEmulator) {
this.emulator = startEmulator ? startEmulator() : null;
this.server =
startPGAdapter(
startEmulator ? "localhost:" + this.emulator.getMappedPort(9010) : null, startEmulator);
}

/**
* Starts PGAdapter in-process and returns a reference to the server. Use this reference to get
* the port number that was dynamically assigned to PGAdapter, and to gracefully shut down the
* server when your application shuts down.
*/
static ProxyServer startPGAdapter() {
private static ProxyServer startPGAdapter(String emulatorHost, boolean autoConfigEmulator) {
// Start PGAdapter using the default credentials of the runtime environment on port a random
// port.
OptionsMetadata.Builder builder = OptionsMetadata.newBuilder().setPort(0);
if (!Strings.isNullOrEmpty(System.getenv("SPANNER_EMULATOR_HOST"))) {
// autoConfigEmulator ensures that PGAdapter automatically sets up a connection that works with
// the Emulator. That means:
// 1. Use plain text instead of SSL.
// 2. Do not use any credentials.
// 3. Automatically create the Spanner instance and database that PGAdapter wants to connect to,
// so no prior setup is required.
if (autoConfigEmulator || !Strings.isNullOrEmpty(System.getenv("SPANNER_EMULATOR_HOST"))) {
builder.autoConfigureEmulator();
}
// Set a custom emulator host if the Emulator was started automatically by this application.
// That means that the Emulator uses a random port number.
Properties properties = new Properties();
if (emulatorHost != null) {
properties.put("endpoint", emulatorHost);
}
OptionsMetadata options = builder.build();
ProxyServer server = new ProxyServer(options);
ProxyServer server = new ProxyServer(options, OpenTelemetry.noop(), properties);
server.startServer();
server.awaitRunning();

// Override the port that is set in the application.properties file with the one that was
// automatically assigned.
// automatically assigned to the in-memory PGAdapter instance.
System.setProperty("pgadapter.port", String.valueOf(server.getLocalPort()));

return server;
}

/** Starts a Docker container that contains the Cloud Spanner Emulator. */
private static GenericContainer<?> startEmulator() {
GenericContainer<?> container =
new GenericContainer<>(DockerImageName.parse("gcr.io/cloud-spanner-emulator/emulator"));
container.addExposedPort(9010);
container.setWaitStrategy(Wait.forListeningPorts(9010));
container.start();

return container;
}

/** Gracefully shuts down PGAdapter. Call this method when the application is stopping. */
void stopPGAdapter() {
synchronized void shutdown() {
if (this.server != null) {
this.server.stopServer();
SpannerPool.closeSpannerPool();
this.server.awaitTerminated();
}
if (this.emulator != null) {
this.emulator.stop();
}
SpannerPool.closeSpannerPool();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// 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.pgadapter.sample;

import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;

/**
* This class is added as a listener to the Spring Boot application and starts PGAdapter before any
* DataSource is created.
*/
class PGAdapterInitializer implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
private PGAdapter pgAdapter;

PGAdapter getPGAdapter() {
return this.pgAdapter;
}

@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
ConfigurableEnvironment environment = event.getEnvironment();
boolean useEmulator =
Boolean.TRUE.equals(environment.getProperty("spanner.use_emulator", Boolean.class));
this.pgAdapter = new PGAdapter(useEmulator);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Random;
import javax.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
Expand Down Expand Up @@ -61,11 +60,15 @@
public class SampleApplication implements CommandLineRunner {
private static final Logger log = LoggerFactory.getLogger(SampleApplication.class);

/**
* {@link PGAdapter} is a small utility class for starting and stopping PGAdapter in-process with
* the application.
*/
private static final PGAdapter pgAdapter = new PGAdapter();
public static void main(String[] args) {
SpringApplication application = new SpringApplication(SampleApplication.class);
// Add an application listener that initializes PGAdapter BEFORE any data source is created
// by Spring.
PGAdapterInitializer pgAdapterInitializer = new PGAdapterInitializer();
application.addListeners(pgAdapterInitializer);
application.run(args).close();
pgAdapterInitializer.getPGAdapter().shutdown();
}

private final SingerService singerService;
private final AlbumService albumService;
Expand Down Expand Up @@ -108,10 +111,6 @@ public SampleApplication(
this.ticketSaleService = ticketSaleService;
}

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

@Override
public void run(String... args) {
// First clear the current tables.
Expand Down Expand Up @@ -179,13 +178,8 @@ void directedRead() {
.build())
.build();
List<Concert> concerts =
directedReadService.executeReadOnlyTransactionWithDirectedRead(options, concertRepository::findAll);
directedReadService.executeReadOnlyTransactionWithDirectedRead(
options, concertRepository::findAll);
log.info("Found {} concerts using a query with directed read options", concerts.size());
}

@PreDestroy
public void onExit() {
// Stop PGAdapter when the application is shut down.
pgAdapter.stopPGAdapter();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
# The example uses the standard PostgreSQL Hibernate dialect.
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect

# This sample by default runs on the Spanner emulator. Disable this to run on a real Spanner
# instance.
spanner.use_emulator=true

# Defining these properties here makes it a bit easier to build the connection string.
# Change these to match your Cloud Spanner PostgreSQL-dialect database.
spanner.project=my-project
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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.pgadapter.sample;

import static org.junit.Assert.assertTrue;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class SampleApplicationTest {

@Test
public void testRunApplication() {
ByteArrayOutputStream outArrayStream = new ByteArrayOutputStream();
PrintStream out = new PrintStream(outArrayStream);
PrintStream originalOut = System.out;
System.setOut(out);
try {
SampleApplication.main(new String[] {});
String output = outArrayStream.toString();
assertTrue(
output, output.contains("Found 51 concerts using a query with directed read options"));
} finally {
System.setOut(originalOut);
}
}
}
Loading