0

I have a spring boot application running with hibernate. In my scheme I have City which can have multiple Resident(s). I have the following method to update the residents of a city:

public void updateResidentsInCity(long cityId) {
        CompletableFuture
                .supplyAsync(() -> residentsRepository.findAllByCityId(cityId))
                .thenApply(residents -> {
                    // update fields of the resident objects
                    updateResidents(residents);
                    return residents;
                })
                .thenAccept(residents -> residentsRepository.saveAll(residents));
}

But this cause really bad performance issues, because the update running on a different thread, so the hibernate session is expired and when i call residentsRepository.saveAll(residents) hibernate needs to fetch all the entities again.

I thought about two approaches to fix this issue and i'm wondering what is best (or maybe there are another approaches, of course):

  1. Just give up the multiple CompletableFuture(s)- putting all the operations under one @Transactional and make blocking calls:
@Transactional
public void updateResidentsInCity(long cityId) {
        final List<Resident> residents = residentsRepository.findAllByCityId(cityId);
        updateResidents(residents);
        residentsRepository.saveAll(residents);
}

Now I can just call updateResidentsInCity() in a different thread:

CompletableFuture.runAsync(() -> updateResidentsInCity(123))

In addition, allow hibernate to make batch update by adding those properties:

spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_updates=true
  1. keep the same code with CompletableFuture(s), but make residentsRepository.findAllByCityId() a readOnly transaction, and implement my own single native query for batch update:
public void updateResidentsInCity(long cityId) {
    CompletableFuture
        .supplyAsync(() -> residentsRepository.findAllByCityId(cityId))  // readOnly transaction
        .thenApply(residents -> {
             updateResidents(residents);
             return residents;
         })
         .thenAccept(residents -> residentsRepository.batchUpdate(residents));  // new transaction, one native query
}

I will be glad to get some insights about which approach is better, and maybe another approaches I didn't think of. Please note that my application potentially should update many cities in the same time.

Tal Niv
  • 3
  • 1

1 Answers1

0

Your first solution with a single @Transactional method called from an async thread is actually a good solution, since all the operations performed in a single transaction. As a side note, you don't need to call residentsRepository.saveAll(residents) when modifying exiting entity instances fetched by Hibernate inside a transaction. save is for new or "detached" entity instances (fetched previously, outside current transaction). See: https://stackoverflow.com/a/46708295

Enabling batching in properties as you did is also very important. Batch size usually can be larger. I use these setting:

spring.jpa.properties.hibernate.jdbc.batch_size=200
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.batch_versioned_data=true

You don't show the logic inside updateResidents(). If this logic is simple enough, e.g. increment counter column for each, or set some constant value in each etc., you can get much better performance by using an update query in the repository, either JPQL or native. Something like this:

@Modifying
@Query("update Resident r set r.someCounter = r.someCounter + 1, r.lastUpdate =:timestamp where r.cityId =:cityId")
void updateResident(@Param("cityId") String cityId, @Param("timestamp") long timestamp);
Pavel D.
  • 191
  • 1
  • 6