英文:
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):
- 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
- Keep the same code with CompletableFuture(s), but make
residentsRepository.findAllByCityId()
areadOnly
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):
- 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
- keep the same code with CompletableFuture(s), but make
residentsRepository.findAllByCityId()
areadOnly
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);
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论