使用CompletableFuture在Spring Boot中获取和更新实体的策略,结合Hibernate。

huangapple go评论70阅读模式
英文:

Strategies to fetch and update entities using CompletableFuture in spring boot with hibernate

问题

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 is 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 other 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 updates 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 other approaches I didn't think of.
Please note that my application potentially should update many cities at the same time.

英文:

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.

答案1

得分: 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.
首次解决方案是在异步线程中调用的单个@Transactional方法,实际上是一个很好的解决方案,因为所有操作都在一个事务中执行。

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 the current transaction). See: https://stackoverflow.com/a/46708295
顺便提一下,当在事务内部通过Hibernate修改已存在的实体实例时,不需要调用residentsRepository.saveAll(residents)save适用于新的或“分离”的实体实例(之前在当前事务之外获取)。参见: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 settings:
启用属性中的批处理与您所做的一样非常重要。批处理大小通常可以更大。我使用以下设置:

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:
您没有显示updateResidents()中的逻辑。如果这个逻辑足够简单,例如为每个计数器列递增,或在每个计数器列中设置某个常量值等,您可以通过在存储库中使用更新查询(JPQL或本机查询)来获得更好的性能。类似于这样:

@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);
英文:

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);

huangapple
  • 本文由 发表于 2023年5月17日 22:01:15
  • 转载请务必保留本文链接:https://go.coder-hub.com/76272961.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定