2

Recently I faced an issue regarding updating entities from a @Scheduled method where it would fail with the exception org.hibernate.TransientPropertyValueException: object references an unsaved transient instance even though it would work seamless when invoked from a @RestController method. This is the relevant example:

The offending method (other parts of the class omitted for brevity):

@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();
    }
}

The @RestController caller (again most stuff omitted):

@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()));
  }
}

Again, this works just fine when used like above. The problem happens when AnonymizationService#anonymizeItemsOlderThan() is instead invoked from the following @Scheduled method:

@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());
  }
}

In this case it fails with the exception mentioned above (org.hibernate.TransientPropertyValueException).

Upon changing the log level to DEBUG and carefully analyzing it, nothing unexpected happens:

  • When the method is invoked from the @RestController an existing EntityManager is used and a transaction created:
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
  • When the method is invoked from the @Scheduled method a new EntityManager is created:
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

Naturally, my instinct was to add @Transactional to the Anonymization#anonymizeItemsOlderThan() method which immediately solved it, but why?

Why does it work in one case and not in the other? Why does the saveAndFlush() must be performed using the same EntityManager used to retrieve the entity in the first place?

This situation made me think my knowledge is flawed on a very basic level, but somehow couldn't find a clear explanation to it. In any case feel free to point me towards relevant literature that might help me.

atgomes
  • 21
  • 1
  • 2
  • 1
    ... https://stackoverflow.com/q/34641670/592355 – xerx593 Mar 16 '20 at 11:45
  • @xerx593 thanks, even though it looks like it doesn't exactly answer my question, it made me realize that my question might be incorrectly formulated. So would I be correct in assuming that the "catch" here is that the `fetch` and the `save` calls should use the same entity manager (which in fact `@Transactional` fixes)? – atgomes Mar 16 '20 at 13:31
  • ..sry, i deleted my previous comment, a la: "The exception/behavior would be (more) reasonable, if: `@RestController` *is* `@Transactional` ..and your `@Service/@Scheduled` *not*. ;)" (fix: make service( OR scheduled(!?)) transactional) – xerx593 Mar 16 '20 at 13:37

0 Answers0