diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index 392436b2d..1114c08de 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -23,6 +23,13 @@ + + com.google.cloud + google-cloud-spanner-bom + 6.91.1 + pom + import + com.google.cloud libraries-bom @@ -37,6 +44,7 @@ com.google.cloud google-cloud-spanner-jdbc + 2.29.1 com.google.api.grpc diff --git a/samples/snippets/src/main/java/com/example/spanner/jdbc/IsolationLevel.java b/samples/snippets/src/main/java/com/example/spanner/jdbc/IsolationLevel.java new file mode 100644 index 000000000..7153b7c7a --- /dev/null +++ b/samples/snippets/src/main/java/com/example/spanner/jdbc/IsolationLevel.java @@ -0,0 +1,73 @@ +/* + * Copyright 2025 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.example.spanner.jdbc; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Properties; + +final class IsolationLevel { + + static void isolationLevel( + final String project, + final String instance, + final String database, + final Properties properties) + throws SQLException { + String url = String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + project, instance, database); + try (Connection connection = DriverManager.getConnection(url, properties)) { + connection.setAutoCommit(false); + + // Spanner supports setting the isolation level to: + // 1. TRANSACTION_SERIALIZABLE (this is the default) + // 2. TRANSACTION_REPEATABLE_READ + + // The following line sets the default isolation level that will be used + // for all read/write transactions on this connection. + connection.setTransactionIsolation( + Connection.TRANSACTION_REPEATABLE_READ); + + // This query will not take any locks when using + // isolation level repeatable read. + try (ResultSet resultSet = connection + .createStatement() + .executeQuery("SELECT SingerId, Active " + + "FROM Singers " + + "ORDER BY LastName")) { + while (resultSet.next()) { + try (PreparedStatement statement = connection.prepareStatement( + "INSERT OR UPDATE INTO SingerHistory " + + "(SingerId, Active, CreatedAt) " + + "VALUES (?, ?, CURRENT_TIMESTAMP)")) { + statement.setLong(1, resultSet.getLong(1)); + statement.setBoolean(2, resultSet.getBoolean(2)); + statement.executeUpdate(); + } + } + } + connection.commit(); + } + } + + private IsolationLevel() { + } +} diff --git a/samples/snippets/src/test/java/com/example/spanner/jdbc/IsolationLevelTest.java b/samples/snippets/src/test/java/com/example/spanner/jdbc/IsolationLevelTest.java new file mode 100644 index 000000000..e1c7bfbe0 --- /dev/null +++ b/samples/snippets/src/test/java/com/example/spanner/jdbc/IsolationLevelTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2025 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.example.spanner.jdbc; + +import static com.example.spanner.jdbc.IsolationLevel.isolationLevel; +import static org.junit.Assume.assumeTrue; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Properties; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.PullPolicy; +import org.testcontainers.utility.DockerImageName; + +@RunWith(JUnit4.class) +public class IsolationLevelTest { + + private static GenericContainer emulator; + + private static final String PROJECT = "emulator-project"; + private static final String INSTANCE = "my-instance"; + private static final String DATABASE = "my-database"; + private static final Properties PROPERTIES = new Properties(); + + @BeforeClass + public static void setupEmulator() throws Exception { + assumeTrue("This test requires Docker", DockerClientFactory.instance().isDockerAvailable()); + + 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(); + + PROPERTIES.setProperty("endpoint", + String.format("localhost:%d", emulator.getMappedPort(9010))); + PROPERTIES.setProperty("autoConfigEmulator", "true"); + + String url = String.format( + "jdbc:cloudspanner:/projects/%s/instances/%s/databases/%s", + PROJECT, INSTANCE, DATABASE); + try (Connection connection = DriverManager.getConnection(url, PROPERTIES)) { + try (Statement statement = connection.createStatement()) { + statement.addBatch( + "CREATE TABLE Singers (" + + "SingerId INT64 PRIMARY KEY, " + + "FirstName STRING(MAX), " + + "LastName STRING(MAX), " + + "Active BOOL)"); + statement.addBatch( + "CREATE TABLE SingerHistory (" + + "SingerId INT64, " + + "Active BOOL, " + + "CreatedAt TIMESTAMP) " + + "PRIMARY KEY (SingerId, CreatedAt)"); + statement.executeBatch(); + } + } + } + + @AfterClass + public static void stopEmulator() { + if (emulator != null) { + emulator.stop(); + } + } + + @Test + public void testIsolationLevel() throws SQLException { + isolationLevel("emulator-project", "my-instance", "my-database", PROPERTIES); + } + +}