diff --git a/google-cloud-spanner-hibernate-dialect/src/test/java/com/google/cloud/spanner/hibernate/HibernateMockSpannerServerTest.java b/google-cloud-spanner-hibernate-dialect/src/test/java/com/google/cloud/spanner/hibernate/HibernateMockSpannerServerTest.java index d96a7871..8ac2f97b 100644 --- a/google-cloud-spanner-hibernate-dialect/src/test/java/com/google/cloud/spanner/hibernate/HibernateMockSpannerServerTest.java +++ b/google-cloud-spanner-hibernate-dialect/src/test/java/com/google/cloud/spanner/hibernate/HibernateMockSpannerServerTest.java @@ -42,6 +42,7 @@ import com.google.spanner.v1.ExecuteSqlRequest; import com.google.spanner.v1.ResultSet; import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.RollbackRequest; import com.google.spanner.v1.StructType; import com.google.spanner.v1.StructType.Field; import com.google.spanner.v1.Type; @@ -339,7 +340,10 @@ public void testHibernatePooledSequenceEntity_fetchesInBatches() { // We should have three read/write transactions: // 1. Two separate transactions for getting the values from the bit-reversed sequence. // 2. A transaction corresponding to the Hibernate transaction. - assertEquals(3, mockSpanner.countRequestsOfType(CommitRequest.class)); + // The transactions for getting values from the bit-reversed sequence are rolled back instead + // of committed, as this prevents the transaction from being aborted on the emulator. + assertEquals(1, mockSpanner.countRequestsOfType(CommitRequest.class)); + assertEquals(2, mockSpanner.countRequestsOfType(RollbackRequest.class)); assertEquals(2, mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream() .filter(request -> request.getSql().equals(getSequenceValuesSql)).count()); @@ -404,9 +408,11 @@ public void testHibernatePooledSequenceEntity_skipsExcludedRange() { } // We should have two read/write transactions: - // 1. A separate transaction for getting the values from the bit-reversed sequence. + // 1. A separate transaction for getting the values from the bit-reversed sequence. This + // transaction is rolled back. // 2. A transaction corresponding to the Hibernate transaction. - assertEquals(2, mockSpanner.countRequestsOfType(CommitRequest.class)); + assertEquals(1, mockSpanner.countRequestsOfType(CommitRequest.class)); + assertEquals(1, mockSpanner.countRequestsOfType(RollbackRequest.class)); assertEquals(1, mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream() .filter(request -> request.getSql().equals(getSequenceValuesSql)).count()); @@ -444,7 +450,7 @@ public void testHibernatePooledSequenceEntity_abortedErrorRetriesSequence() { Statement.of(getSequenceValuesSql), createBitReversedSequenceResultSet(20001L, 20005L), createBitReversedSequenceResultSet(20006L, 20010L))); - mockSpanner.setCommitExecutionTime(SimulatedExecutionTime.ofException( + mockSpanner.setExecuteStreamingSqlExecutionTime(SimulatedExecutionTime.ofException( mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); String insertSql = "insert into `test-entity` (name,id) values (@p1,@p2)"; @@ -466,7 +472,12 @@ public void testHibernatePooledSequenceEntity_abortedErrorRetriesSequence() { // 1. Two transactions for getting the values from the bit-reversed sequence. The first one is // aborted. // 2. A transaction corresponding to the Hibernate transaction. - assertEquals(3, mockSpanner.countRequestsOfType(CommitRequest.class)); + // The transactions for getting values from the bit-reversed sequence are rolled back instead + // of committed, as this prevents the transaction from being aborted on the emulator. Only the + // transaction that successfully fetched a value from the bit-reversed sequence is rolled back. + // The aborted transaction is neither committed or rolled back. + assertEquals(1, mockSpanner.countRequestsOfType(CommitRequest.class)); + assertEquals(1, mockSpanner.countRequestsOfType(RollbackRequest.class)); // We should have two attempts to get sequence values. assertEquals(2, mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream() diff --git a/google-cloud-spanner-hibernate-dialect/src/test/java/com/google/cloud/spanner/hibernate/SchemaGenerationMockServerTest.java b/google-cloud-spanner-hibernate-dialect/src/test/java/com/google/cloud/spanner/hibernate/SchemaGenerationMockServerTest.java index 5f06e212..0a454b20 100644 --- a/google-cloud-spanner-hibernate-dialect/src/test/java/com/google/cloud/spanner/hibernate/SchemaGenerationMockServerTest.java +++ b/google-cloud-spanner-hibernate-dialect/src/test/java/com/google/cloud/spanner/hibernate/SchemaGenerationMockServerTest.java @@ -50,6 +50,7 @@ import com.google.spanner.v1.ExecuteSqlRequest; import com.google.spanner.v1.ResultSet; import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.RollbackRequest; import com.google.spanner.v1.StructType; import com.google.spanner.v1.StructType.Field; import com.google.spanner.v1.Type; @@ -669,7 +670,8 @@ public void testBatchedSequenceEntity_CreateOnly() { assertEquals(2, insertRequest.getStatementsCount()); assertEquals(insertSql, insertRequest.getStatements(0).getSql()); assertEquals(insertSql, insertRequest.getStatements(1).getSql()); - assertEquals(2, mockSpanner.countRequestsOfType(CommitRequest.class)); + assertEquals(1, mockSpanner.countRequestsOfType(CommitRequest.class)); + assertEquals(1, mockSpanner.countRequestsOfType(RollbackRequest.class)); // Check the DDL statements that were generated. List requests = @@ -789,7 +791,8 @@ public void testBatchedSequenceEntity_Update() { assertEquals(2, insertRequest.getStatementsCount()); assertEquals(insertSql, insertRequest.getStatements(0).getSql()); assertEquals(insertSql, insertRequest.getStatements(1).getSql()); - assertEquals(2, mockSpanner.countRequestsOfType(CommitRequest.class)); + assertEquals(1, mockSpanner.countRequestsOfType(CommitRequest.class)); + assertEquals(1, mockSpanner.countRequestsOfType(RollbackRequest.class)); // Check that there were no DDL statements generated as the data model is up-to-date. List requests = diff --git a/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/README.adoc b/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/README.adoc index 50de9122..49e55ff9 100644 --- a/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/README.adoc +++ b/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/README.adoc @@ -30,3 +30,22 @@ In the `application.properties` file, you'll see that the application is running On the very first time you run the app, Hibernate will automatically create the schema and missing tables based on the `@Entity` definitions. + You can view the data that was populated in your Cloud Spanner database by navigating to your database in the http://console.cloud.google.com/spanner[Spanner Console] view. + +== Important Features +This sample application showcases how to use the following features: + +1. How to use the emulator for local development. See the + link:src/main/resources/application.properties[application.properties] file for an example of how + this is set up. + +2. Use auto-generated UUIDs as primary keys. See the + link:src/main/java/com/google/cloud/spanner/sample/entities/AbstractNonInterleavedEntity.java[AbstractNonInterleavedEntity.java] + entity for an example. + +3. Use a bit-reversed sequence to generate a numerical primary key value. See the + link:src/main/java/com/google/cloud/spanner/sample/entities/TicketSale.java[TicketSale.java] + entity for an example. + +4. Execute read-only transactions. See link:src/main/java/com/google/cloud/spanner/sample/service/SingerService.java[SingerService.java] for an example. + +5. Execute a stale read. See link:src/main/java/com/google/cloud/spanner/sample/service/StaleReadService.java[StaleReadService.java] for an example. diff --git a/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/SampleApplication.java b/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/SampleApplication.java index 595a38ed..d935a643 100644 --- a/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/SampleApplication.java +++ b/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/SampleApplication.java @@ -27,6 +27,7 @@ import com.google.cloud.spanner.sample.service.ConcertService; import com.google.cloud.spanner.sample.service.SingerService; import com.google.cloud.spanner.sample.service.StaleReadService; +import com.google.cloud.spanner.sample.service.TicketSaleService; import com.google.cloud.spanner.sample.service.TrackService; import com.google.cloud.spanner.sample.service.VenueService; import jakarta.annotation.PreDestroy; @@ -62,6 +63,7 @@ public class SampleApplication implements CommandLineRunner { private final TrackService trackService; private final VenueService venueService; private final ConcertService concertService; + private final TicketSaleService ticketSaleService; /** * The {@link StaleReadService} is a generic service that can be used to execute workloads using * stale reads. Stale reads can perform better than strong reads. See seats; + + public TicketSale() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Concert getConcert() { + return concert; + } + + public void setConcert(Concert concert) { + this.concert = concert; + } + + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + + public BigDecimal getPrice() { + return price; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } + + public List getSeats() { + return seats; + } + + public void setSeats(List seats) { + this.seats = seats; + } +} diff --git a/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/repository/ConcertRepository.java b/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/repository/ConcertRepository.java index 929fc64e..26b09036 100644 --- a/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/repository/ConcertRepository.java +++ b/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/repository/ConcertRepository.java @@ -21,6 +21,6 @@ import com.google.cloud.spanner.sample.entities.Concert; import org.springframework.data.jpa.repository.JpaRepository; -public interface ConcertRepository extends JpaRepository { +public interface ConcertRepository extends JpaRepository { } diff --git a/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/repository/TicketSaleRepository.java b/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/repository/TicketSaleRepository.java new file mode 100644 index 00000000..0581eb8e --- /dev/null +++ b/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/repository/TicketSaleRepository.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019-2024 Google LLC + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +package com.google.cloud.spanner.sample.repository; + +import com.google.cloud.spanner.sample.entities.TicketSale; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TicketSaleRepository extends JpaRepository { + +} diff --git a/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/repository/VenueRepository.java b/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/repository/VenueRepository.java index 8e586abc..2f077bd4 100644 --- a/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/repository/VenueRepository.java +++ b/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/repository/VenueRepository.java @@ -21,6 +21,6 @@ import com.google.cloud.spanner.sample.entities.Venue; import org.springframework.data.jpa.repository.JpaRepository; -public interface VenueRepository extends JpaRepository { +public interface VenueRepository extends JpaRepository { } diff --git a/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/service/TicketSaleService.java b/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/service/TicketSaleService.java new file mode 100644 index 00000000..4fac1a22 --- /dev/null +++ b/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/main/java/com/google/cloud/spanner/sample/service/TicketSaleService.java @@ -0,0 +1,96 @@ +/* + * Copyright 2019-2024 Google LLC + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +package com.google.cloud.spanner.sample.service; + +import com.google.cloud.spanner.sample.entities.Concert; +import com.google.cloud.spanner.sample.entities.TicketSale; +import com.google.cloud.spanner.sample.repository.ConcertRepository; +import com.google.cloud.spanner.sample.repository.TicketSaleRepository; +import jakarta.transaction.Transactional; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.springframework.stereotype.Service; + +/** + * Service class for fetching and saving TicketSale records. + */ +@Service +public class TicketSaleService { + + private final TicketSaleRepository repository; + + private final ConcertRepository concertRepository; + + private final RandomDataService randomDataService; + + /** + * Constructor with auto-injected dependencies. + */ + public TicketSaleService( + TicketSaleRepository repository, + ConcertRepository concertRepository, + RandomDataService randomDataService) { + this.repository = repository; + this.concertRepository = concertRepository; + this.randomDataService = randomDataService; + } + + /** + * Deletes all TicketSale records in the database. + */ + @Transactional + public void deleteAllTicketSales() { + repository.deleteAll(); + } + + /** + * Generates the specified number of random TicketSale records. + */ + @Transactional + public List generateRandomTicketSales(int count) { + Random random = new Random(); + + List concerts = concertRepository.findAll(); + List ticketSales = new ArrayList<>(count); + if (concerts.isEmpty()) { + return ticketSales; + } + for (int i = 0; i < count; i++) { + TicketSale ticketSale = new TicketSale(); + ticketSale.setConcert(concerts.get(random.nextInt(concerts.size()))); + ticketSale.setCustomerName( + randomDataService.getRandomFirstName() + + " " + + randomDataService.getRandomLastName()); + ticketSale.setPrice( + BigDecimal.valueOf(random.nextDouble() * 300).setScale(2, RoundingMode.HALF_UP)); + int numSeats = random.nextInt(5) + 1; + List seats = new ArrayList<>(numSeats); + for (int n = 0; n < numSeats; n++) { + seats.add("A" + random.nextInt(100) + 1); + } + ticketSale.setSeats(seats); + ticketSales.add(ticketSale); + } + return repository.saveAll(ticketSales); + } +} diff --git a/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/test/java/com/google/cloud/spanner/sample/SampleApplicationMockServerTest.java b/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/test/java/com/google/cloud/spanner/sample/SampleApplicationMockServerTest.java index 32875ebe..a3c60217 100644 --- a/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/test/java/com/google/cloud/spanner/sample/SampleApplicationMockServerTest.java +++ b/google-cloud-spanner-hibernate-samples/spring-data-jpa-full-sample/src/test/java/com/google/cloud/spanner/sample/SampleApplicationMockServerTest.java @@ -177,6 +177,8 @@ public static void setupQueryResults() { .to("%").bind("p2").to("%").bind("p3").to("%").bind("p4").to("%").build(), empty())); // Add results for the initial queries that are used to delete all data. + mockSpanner.putStatementResults(StatementResult.query(Statement.of("select ts1_0.id,ts1_0.concert_id,ts1_0.created_at,ts1_0.customer_name,ts1_0.price,ts1_0.seats,ts1_0.updated_at from ticket_sale ts1_0"), + empty())); mockSpanner.putStatementResult(StatementResult.query(Statement.of( "select c1_0.id,c1_0.created_at,c1_0.end_time,c1_0.name,c1_0.singer_id,c1_0.start_time,c1_0.updated_at,c1_0.venue_id from concert c1_0"), empty())); @@ -399,10 +401,10 @@ public void testRunApplication() { System.setProperty("spanner.auto_tag_transactions", "true"); SpringApplication.run(SampleApplication.class).close(); - assertEquals(31, mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream() + assertEquals(35, mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream() .filter(request -> !request.getSql().equals("SELECT 1")).count()); assertEquals(6, mockSpanner.countRequestsOfType(ExecuteBatchDmlRequest.class)); - assertEquals(9, mockSpanner.countRequestsOfType(CommitRequest.class)); + assertEquals(11, mockSpanner.countRequestsOfType(CommitRequest.class)); // Verify that we receive a transaction tag for the generateRandomVenues() method. assertEquals(1, mockSpanner.getRequestsOfType(ExecuteBatchDmlRequest.class).stream() diff --git a/google-cloud-spanner-hibernate-tools/src/main/java/com/google/cloud/spanner/hibernate/PooledBitReversedSequenceStyleGenerator.java b/google-cloud-spanner-hibernate-tools/src/main/java/com/google/cloud/spanner/hibernate/PooledBitReversedSequenceStyleGenerator.java index b05bf4ef..9df83a67 100644 --- a/google-cloud-spanner-hibernate-tools/src/main/java/com/google/cloud/spanner/hibernate/PooledBitReversedSequenceStyleGenerator.java +++ b/google-cloud-spanner-hibernate-tools/src/main/java/com/google/cloud/spanner/hibernate/PooledBitReversedSequenceStyleGenerator.java @@ -402,7 +402,13 @@ private Iterator fetchIdentifiers(SharedSessionContractImplementor session } } } - connection.commit(); + // Do a rollback instead of a commit here because: + // 1. We have only accessed a bit-reversed sequence during the transaction. + // 2. Committing or rolling back the transaction does not make any difference for the + // sequence. Its state has been updated in both cases. + // 3. Committing the transaction on the emulator would cause it to be aborted, as the + // emulator only supports one transaction at any time. Rolling back is however allowed. + connection.rollback(); return identifiers.iterator(); } } catch (SQLException sqlException) {