Skip to content

Commit

Permalink
feat: support bit-reversed sequences on the emulator (#984)
Browse files Browse the repository at this point in the history
* fix: JSON column DDL was generated as STRING

* feat: support bit-reversed sequences on the emulator

Bit-reversed sequences require the use of a read/write transaction.
Hibernate starts a second read/write transaction when it needs to
fetch new identifiers, and this transaction is aborted when it is
running on the emulator. The PooledBitReversedSequenceGenerator
now solves this by rolling back that transaction after getting the
sequence values. This is semantically equivalent to committing the
transaction, as the state of the sequence is durably changed
regardless whether the transaction is committed or rolled back.

Fixes #983

* test: use JSON null values as well
  • Loading branch information
olavloite authored Mar 28, 2024
1 parent aae5838 commit 2e56037
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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)";
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<UpdateDatabaseDdlRequest> requests =
Expand Down Expand Up @@ -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<UpdateDatabaseDdlRequest> requests =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <a
Expand All @@ -81,13 +83,15 @@ public SampleApplication(
TrackService trackService,
VenueService venueService,
ConcertService concertService,
TicketSaleService ticketSaleService,
StaleReadService staleReadService,
ConcertRepository concertRepository) {
this.singerService = singerService;
this.albumService = albumService;
this.trackService = trackService;
this.venueService = venueService;
this.concertService = concertService;
this.ticketSaleService = ticketSaleService;
this.staleReadService = staleReadService;
this.concertRepository = concertRepository;
}
Expand All @@ -100,6 +104,7 @@ public static void main(String[] args) {
public void run(String... args) {
// First clear the current tables.
log.info("Deleting all existing data");
ticketSaleService.deleteAllTicketSales();
concertService.deleteAllConcerts();
albumService.deleteAllAlbums();
singerService.deleteAllSingers();
Expand All @@ -115,6 +120,8 @@ public void run(String... args) {
log.info("Created 20 venues");
concertService.generateRandomConcerts(50);
log.info("Created 50 concerts");
ticketSaleService.generateRandomTicketSales(250);
log.info("Created 250 ticket sales");

// Print some of the randomly inserted data.
printData();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* 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.entities;

import com.google.cloud.spanner.hibernate.PooledBitReversedSequenceStyleGenerator;
import com.google.cloud.spanner.hibernate.types.SpannerStringArray;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import java.math.BigDecimal;
import java.util.List;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;
import org.hibernate.annotations.Type;
import org.hibernate.id.enhanced.SequenceStyleGenerator;

/** {@link TicketSale} shows how to use bit-reversed sequences to generate primary key values. */
@Entity
public class TicketSale extends AbstractEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ticketSaleId")
@GenericGenerator(
name = "ticketSaleId",
// Use this custom strategy to ensure the use of a bit-reversed sequence that is compatible
// with batching multiple inserts. See also
// https://docs.jboss.org/hibernate/orm/6.3/userguide/html_single/Hibernate_User_Guide.html#batch.
type = PooledBitReversedSequenceStyleGenerator.class,
parameters = {
// Use a separate sequence name for each entity.
@Parameter(name = SequenceStyleGenerator.SEQUENCE_PARAM, value = "ticket_sale_seq"),
// The increment_size is not actually set on the sequence that is created, but is used to
// generate a SELECT query that fetches this number of identifiers at once.
@Parameter(name = SequenceStyleGenerator.INCREMENT_PARAM, value = "200"),
@Parameter(name = SequenceStyleGenerator.INITIAL_PARAM, value = "50000"),
// Add any range that should be excluded by the generator if your table already
// contains existing values that have been generated by other generators.
@Parameter(name = PooledBitReversedSequenceStyleGenerator.EXCLUDE_RANGE_PARAM,
value = "[1,1000]"),
})
private long id;

@ManyToOne(optional = false, fetch = FetchType.LAZY)
private Concert concert;

private String customerName;

private BigDecimal price;

@Type(SpannerStringArray.class)
private List<String> 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<String> getSeats() {
return seats;
}

public void setSeats(List<String> seats) {
this.seats = seats;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
import com.google.cloud.spanner.sample.entities.Concert;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ConcertRepository extends JpaRepository<Concert, Long> {
public interface ConcertRepository extends JpaRepository<Concert, String> {

}
Original file line number Diff line number Diff line change
@@ -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<TicketSale, Long> {

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
import com.google.cloud.spanner.sample.entities.Venue;
import org.springframework.data.jpa.repository.JpaRepository;

public interface VenueRepository extends JpaRepository<Venue, Long> {
public interface VenueRepository extends JpaRepository<Venue, String> {

}
Loading

0 comments on commit 2e56037

Please sign in to comment.