GoogleCloudPlatform / GoogleCloudPlatform/spring-cloud-gcp

[Spanner] Request to implement the FOR UPDATE clause when querying inside a read-write transaction

Open
#3,984 1 comment 0 reactions 0 assignees View on GitHub
priority: p3 type: enhancement
Dominant language
Java
Stars
551
Forks
349
Avg merge
1d 13h
Merged PRs (30d)
14

Description

This feature request is related to a problem I'm having when using SpannerRepository. When I declare an `@Transactional` annotated method, which calls a repository find method, modifies some fields, then saves, and I invoke this method for the same entity with a high frequency, I find that many of the transactions are aborted.

I would like for _some_ way to specify the following: when in a read-write transaction, use the `FOR UPDATE` clause when querying for records within that transaction. Perhaps this can be specified using some configuration or annotation like `@Lock` on the `SpannerRepository` method. Doc: https://cloud.google.com/spanner/docs/use-select-for-update

I've solved this by implementing my own repository method using `@Query` to include the `FOR UPDATE` clause. This is limiting when `@Interleaved` child entities are loaded eagerly, but not locked using `FOR UPDATE`. This means I am executing additional queries to fetch the child entities with `FOR UPDATE`. I've also used the `SpannerOperations` object to execute statements with this clause. However I'm not experienced enough to figure out the joins and how to map the query result to the entity class which has children.

Thank you for providing and maintaining this repository. I apologize if this has already been considered, or is an unreasonable request. This is my first time making a request such as this, so please be gentle :D

Here are some code snippets:
In some Spring component:
```
public void testTheThing() {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
log.info("start spamming transactions");
int transactionsCount = 10;
while (transactionsCount > 0) {
executor.submit(() -> {
try {
myServiceBean.testTheThing("stringpk", 1234L);
} catch (Exception e) {
log.error("testTheThing Exception {}", e.getMessage(), e);
}
});
transactionsCount--;
}
}
}
```
and in a separate Spring Component:
```
@Transactional
public void testTheThing(String stringPK, Long longPK) throws InterruptedException {
var entity = myRepository.findById(Key.of(stringPK, longPK)).orElseThrow();
// var entity = myRepository.findByIdForUpdate(stringPK, longPK).orElseThrow();
try {Thread.sleep(20); } catch (Exception ignored) {} // imagine some statements here
entity.setSomeStringValue(UUID.randomUUID().toString());
myRepository.save(entity);
}
```
and lastly in the Repository:
```
@Query("SELECT * FROM my_table WHERE string_pk = @stringPK and long_pk = @longPK FOR UPDATE")
Optional findByIdForUpdate(String stringPK, Long longPK);
```

using the `findByIdForUpdate` will not create any Exceptions, whereas with the `findById`, all but one transaction is aborted with an Exception

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.