Почему метод сохранения должен использовать уже существующий EntityManager? - PullRequest
2 голосов
/ 16 марта 2020

Недавно я столкнулся с проблемой обновления сущностей из метода @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 уровне, но как-то не мог найти четкого объяснения этому. В любом случае, не стесняйтесь указывать мне на соответствующую литературу, которая может мне помочь.

...