为什么保存方法需要使用已经存在的EntityManager?

huangapple 未分类评论46阅读模式
标题翻译

Why does save method needs to use an already existing EntityManager?

问题

最近我遇到了一个问题,涉及从一个 @Scheduled 方法中更新实体,在这个方法中,尽管从 @RestController 方法中调用时可以无缝运行,但会失败并抛出异常 org.hibernate.TransientPropertyValueException: object references an unsaved transient instance。以下是相关示例:

有问题的方法(类的其他部分已省略以保持简洁):

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

与上述用法一样,这在使用时是完全正常的。问题发生在从以下 @Scheduled 方法中调用 AnonymizationService#anonymizeItemsOlderThan() 时:

@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

当然,我的直觉是在 Anonymization#anonymizeItemsOlderThan() 方法上添加 @Transactional 注解,这立即解决了问题,但是为什么呢?

为什么在一种情况下可以工作,在另一种情况下不行?为什么必须使用与首次检索实体时使用的相同的 EntityManager 执行 saveAndFlush() 操作?

这种情况让我觉得我的基础知识有问题,但我却无法找到一个清晰的解释。无论如何,如果有相关的文献可以帮助我,欢迎指引我。

英文翻译

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&lt;Item&gt; data = itemRepository.findAllByCreatedDateBeforeAndAnonymizationDateIsNull(Instant.now().minus(days, ChronoUnit.DAYS));

        List&lt;String&gt; itemsAnonymized = new ArrayList&lt;&gt;(data.size());

        data.forEach(item -&gt; 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 = &quot;${datadeletion.path:/anonymize}&quot;, produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity&lt;Result&gt; 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 = &quot;${datadeletion.anonymization.schedule}&quot;)
  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&lt;open&gt;)] 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&lt;open&gt;)] 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.

huangapple
  • 本文由 发表于 2020年3月16日 19:29:50
  • 转载请务必保留本文链接:https://java.coder-hub.com/60705173.html
匿名

发表评论

匿名网友

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

确定