2

I have the following method that adds a player to the database or updates it, should it already exist:

@Transactional(isolation = Isolation.SERIALIZABLE)
public Player addOrUpdatePlayer(String playerId, String playerName) {
    Optional<Player> playerOptional = playerRepository.findById(playerId);
    if (playerOptional.isPresent()) {
        Player player = playerOptional.get();
        player.setName(playerName);
        return playerRepository.save(player);
    } else {
        Player newPlayer = Player.builder()
            .id(playerId)
            .name(playerName)
            .build();
        return playerRepository.save(newPlayer);
    }
}

This method is sometimes called by two different threads and therefore executed two times at almost the same time. Sometimes I get the following exception:

org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Unique index or primary key violation: "PUBLIC.PRIMARY_KEY_B42 ON PUBLIC.PLAYER(ID) VALUES 3";

This makes sense to me because both methods check in the beginning if the player is in the database and it is not in both cases. Then both try to insert and the slower one runs into the constraint.

I want to lock the player table during the execution of this method, so if it is called multiple times, they have to wait for each other.

My player repository looks like this:

public interface PlayerRepository extends JpaRepository<Player, String> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional<Player> findById(String var1);

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    <S extends Player> S save(S var1);
}

I also tried to increase isolation level on the transaction:

@Transactional(isolation = Isolation.SERIALIZABLE)

However, I still get this exception, so apparently the inserts are still running concurrently. What am I doing wrong? Or is there a different way to achieve what i am trying here?

After some more research, I think I understand why neither @Lock nor @Transactional(isolation = Isolation.SERIALIZABLE) are doing anything. They basically work by locking records in a database table for the duration of the transaction, but since there are no records to lock in my case, its not doing anything.

So, I guess I would have to lock the whole table for that transaction. Is that even possible with Spring JPA? And since that doesn't seem smart performance wise, is there an other way to tackle this problem?

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Philipp
  • 378
  • 1
  • 2
  • 12
  • You need to fix your id generation strategy - marking as duplicate. – mikeb Sep 29 '21 at 15:22
  • Does this answer your question? [JPA primary key auto generate](https://stackoverflow.com/questions/1817625/jpa-primary-key-auto-generate) – mikeb Sep 29 '21 at 15:23
  • Maybe i dont get what you mean, but i dont generate my ids. In my case, it is necessary that i set a custom id (`playerId`) – Philipp Sep 29 '21 at 15:37
  • Likely a better approach would be to **retry** the unit of work. – Mark Rotteveel Sep 30 '21 at 10:41
  • @MarkRotteveel You mean i could catch the DataIntegrityViolationException and just try again and then it would update the existing record? This would be possible, but the scenario happens quite often. So i would catch this exception a lot of times and it wouldnt be an 'Execption' anymore, but part of my business logic. That doesnt seem right to me and also probably bad for performance. – Philipp Sep 30 '21 at 10:44
  • Then you should look into why two different threads are working on the same player at the same time. That sounds like a serious flaw to me. Or you need to switch to using something like SQL `MERGE`. – Mark Rotteveel Sep 30 '21 at 10:49
  • The two different threads are each triggered by the connection to a different websocket. In some cases the frontend app connects to these websockets at the same time. But i will look into `MERGE` and maybe use a native query with that. Tank you! – Philipp Sep 30 '21 at 11:12

1 Answers1

2

You could use @SQLInsert for this purpose to change the SQL that is used for the insert operation. Depending on your database, it might be possible to switch to some database native conflict/merge handling. See Hibernate Transactions and Concurrency Using attachDirty (saveOrUpdate)

Christian Beikov
  • 15,141
  • 2
  • 32
  • 58
  • Thank you Christian! Similar to what you suggested, I ended up defining a native query to upsert in my repository like this: `@Query(value = "INSERT INTO PLAYER (...) VALUES(...) ON CONFLICT (ID) DO UPDATE ....", nativeQuery = true)`. But `@SQLInsert` looks like an elegant solution aswell, I didn't know it before. – Philipp Oct 14 '21 at 10:44