Недавно я столкнулся с проблемой обновления сущностей из метода @Scheduled
, в которой произошел сбой с исключением org.hibernate.TransientPropertyValueException: object references an unsaved transient instance
, даже если он будет работать без проблем при вызове из метода @RestController
. Вот соответствующий пример:
Метод-нарушитель (другие части класса для краткости опущены):
@Service
public class AnonymizationService
{
private final ItemRepository itemRepository;
public Result anonymizeItemsOlderThan(int days) {
List<Item> data = itemRepository.findAllByCreatedDateBeforeAndAnonymizationDateIsNull(Instant.now().minus(days, ChronoUnit.DAYS));
List<String> itemsAnonymized = new ArrayList<>(data.size());
data.forEach(item -> itemsAnonymized.add(itemRepository.save(item.anonymize()).getRequestId()));
return Result.builder().anonymizedItems(itemsAnonymized).build();
}
}
Вызывающий @RestController
( опять большинство вещей опущено):
@RestController
public class DataAnonymizationAPI
{
private final AnonymizationService anonymizationService;
@PutMapping(path = "${datadeletion.path:/anonymize}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Result> anonymizeAll(@Valid DataDeletionRules dataDeletionRules) {
return ResponseEntity.ok(anonymizationService.anonymizeItemsOlderThan(dataDeletionRules.getMinimunAge()));
}
}
Опять же, это работает просто отлично, когда используется, как указано выше. Проблема возникает, когда вместо этого AnonymizationService#anonymizeItemsOlderThan()
вызывается из следующего @Scheduled
метода:
@Component
public class DataDeletionTasks
{
private final AnonymizationService anonymizationService;
private final DataAnonymizationProperties properties;
@Scheduled(cron = "${datadeletion.anonymization.schedule}")
public void anonymizeItemsPeriodically() {
anonymizationService.anonymizeItemsOlderThan(properties.getAnonymization().getMinAge());
}
}
В этом случае происходит сбой с исключением, упомянутым выше (org.hibernate.TransientPropertyValueException
).
При изменяя уровень журнала на DEBUG и тщательно анализируя его, ничего неожиданного не происходит:
- Когда метод вызывается из
@RestController
, используется существующий EntityManager
и создается транзакция:
o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(1702787226<open>)] for JPA transaction
o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAndFlush]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
- Когда метод вызывается из метода
@Scheduled
, создается новый EntityManager
:
o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAndFlush]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.orm.jpa.JpaTransactionManager : Opened new EntityManager [SessionImpl(644498403<open>)] for JPA transaction
Естественно, мой инстинкт должен был добавить @Transactional
к методу Anonymization#anonymizeItemsOlderThan()
, который сразу решил ее, но почему?
Почему это работает в одном случае, а не в другом? Почему saveAndFlush()
должен выполняться с использованием того же EntityManager
, который использовался для извлечения сущности в первую очередь?
Эта ситуация заставила меня думать, что мои знания несовершенны на очень базовом c уровне, но как-то не мог найти четкого объяснения этому. В любом случае, не стесняйтесь указывать мне на соответствующую литературу, которая может мне помочь.