Skip to content

Commit

Permalink
docs: add insert-or-update example
Browse files Browse the repository at this point in the history
  • Loading branch information
olavloite committed Sep 2, 2024
1 parent 81c9189 commit 459305a
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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 Down Expand Up @@ -138,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
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
Expand Up @@ -49,6 +49,7 @@
import com.google.spanner.v1.TypeAnnotationCode;
import com.google.spanner.v1.TypeCode;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
Expand Down Expand Up @@ -352,6 +353,76 @@ public static void setupQueryResults() {
.collect(Collectors.toList()))
.build()));
}
int singerIndex = ThreadLocalRandom.current().nextInt(INITIAL_SINGERS.size());
Singer randomSinger = INITIAL_SINGERS.get(singerIndex);
mockSpanner.putStatementResult(
StatementResult.query(
Statement.of("SELECT * FROM singers ORDER BY sha256(last_name::bytea) LIMIT 1"),
ResultSet.newBuilder()
.setMetadata(
ResultSetMetadata.newBuilder()
.setRowType(
StructType.newBuilder()
.addFields(
Field.newBuilder()
.setName("id")
.setType(Type.newBuilder().setCode(TypeCode.INT64).build())
.build())
.addFields(
Field.newBuilder()
.setName("active")
.setType(Type.newBuilder().setCode(TypeCode.BOOL).build())
.build())
.addFields(
Field.newBuilder()
.setName("last_name")
.setType(Type.newBuilder().setCode(TypeCode.STRING).build())
.build())
.addFields(
Field.newBuilder()
.setName("full_name")
.setType(Type.newBuilder().setCode(TypeCode.STRING).build())
.build())
.addFields(
Field.newBuilder()
.setName("updated_at")
.setType(
Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
.build())
.addFields(
Field.newBuilder()
.setName("created_at")
.setType(
Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
.build())
.addFields(
Field.newBuilder()
.setName("first_name")
.setType(Type.newBuilder().setCode(TypeCode.STRING).build())
.build())
.build())
.build())
.addRows(
ListValue.newBuilder()
.addValues(
Value.newBuilder()
.setStringValue(String.valueOf(Long.reverse(singerIndex + 1)))
.build())
.addValues(Value.newBuilder().setBoolValue(true).build())
.addValues(
Value.newBuilder().setStringValue(randomSinger.getLastName()).build())
.addValues(
Value.newBuilder()
.setStringValue(
randomSinger.getFirstName() + " " + randomSinger.getLastName())
.build())
.addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
.addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
.addValues(
Value.newBuilder().setStringValue(randomSinger.getFirstName()).build())
.build())
.build()));

mockSpanner.putStatementResult(
StatementResult.query(
Statement.newBuilder(
Expand Down Expand Up @@ -726,6 +797,75 @@ public static void setupQueryResults() {
.build())
.collect(Collectors.toList()))
.build()));
mockSpanner.putPartialStatementResult(
StatementResult.query(
Statement.of(
"INSERT INTO singers (id, first_name, last_name, active) VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET id=excluded.id, first_name=excluded.first_name, last_name=excluded.last_name, active=excluded.active\n"
+ "RETURNING *"),
ResultSet.newBuilder()
.setMetadata(
ResultSetMetadata.newBuilder()
.setRowType(
StructType.newBuilder()
.addFields(
Field.newBuilder()
.setName("id")
.setType(
Type.newBuilder().setCode(TypeCode.INT64).build())
.build())
.addFields(
Field.newBuilder()
.setName("active")
.setType(Type.newBuilder().setCode(TypeCode.BOOL).build())
.build())
.addFields(
Field.newBuilder()
.setName("last_name")
.setType(
Type.newBuilder().setCode(TypeCode.STRING).build())
.build())
.addFields(
Field.newBuilder()
.setName("full_name")
.setType(
Type.newBuilder().setCode(TypeCode.STRING).build())
.build())
.addFields(
Field.newBuilder()
.setName("updated_at")
.setType(
Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
.build())
.addFields(
Field.newBuilder()
.setName("created_at")
.setType(
Type.newBuilder().setCode(TypeCode.TIMESTAMP).build())
.build())
.addFields(
Field.newBuilder()
.setName("first_name")
.setType(
Type.newBuilder().setCode(TypeCode.STRING).build())
.build())
.build())
.build())
.addRows(
ListValue.newBuilder()
.addValues(
Value.newBuilder()
.setStringValue(
String.valueOf(ThreadLocalRandom.current().nextLong()))
.build())
.addValues(Value.newBuilder().setBoolValue(true).build())
.addValues(Value.newBuilder().setStringValue("Russel").build())
.addValues(Value.newBuilder().setStringValue("Beatriz Russel").build())
.addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
.addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build())
.addValues(Value.newBuilder().setStringValue("Beatriz").build())
.build())
.setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build())
.build()));
}
}

Expand All @@ -737,12 +877,18 @@ public void testRunApplication() {
SpringApplication.run(Application.class).close();

assertEquals(
39,
40,
mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
.filter(request -> !request.getSql().equals("SELECT 1"))
.filter(
request ->
!request.getSql().equals("SELECT 1")
&& !request
.getSql()
.equals(
"SELECT * FROM singers ORDER BY sha256(last_name::bytea) LIMIT 1"))
.count());
assertEquals(3, mockSpanner.countRequestsOfType(ExecuteBatchDmlRequest.class));
assertEquals(5, mockSpanner.countRequestsOfType(CommitRequest.class));
assertEquals(6, mockSpanner.countRequestsOfType(CommitRequest.class));

// Verify that the service methods use transactions.
String insertSingerSql =
Expand Down

0 comments on commit 459305a

Please sign in to comment.