为啥“findById()”在同一实体上调用 getOne() 后返回代理?

Posted

技术标签:

【中文标题】为啥“findById()”在同一实体上调用 getOne() 后返回代理?【英文标题】:Why "findById()" returns proxy after calling getOne() on same entity?为什么“findById()”在同一实体上调用 getOne() 后返回代理? 【发布时间】:2020-02-18 21:46:29 【问题描述】:

在我的网络应用程序中,在服务布局中,我使用“餐厅”实体的代理(“餐厅”字段上的 FetchType.Lazy)。

  User user = userRepository.get(userId);
  /*
     Getting proxy here, not restaurant object
  */
  Restaurant userRestaurantRef = user.getRestaurant();

  if (userRestaurantRef != null)
     restaurantRepository.decreaseRating(userRestaurantRef.getId());
  

  restaurantRepository.increaseRating(restaurantId);
  /*
    "getReference" invokes "getOne()"
  */
  user.setRestaurant(restaurantRepository.getReference(restaurantId));
  userRepository.save(user);

在测试中通过控制器调用此方法后,所有其他 RestaurantRepository 的获取方法(如 findById())都返回 代理也。

但是,如果我在我的服务方法之前调用“findById()”方法,那就没问题了。

例如:

mockMvc.perform(put(REST_URL + RESTAURANT1_ID)
                .param("time", "10:30")
                .with(userHttpBasic(USER)))
                .andExpect(status().isNoContent());

Restaurant restaurant = restaurantRepository.get(RESTAURANT1_ID);

“餐厅”是代理

Restaurant restaurantBefore = restaurantRepository.get(RESTAURANT1_ID);

mockMvc.perform(put(REST_URL + RESTAURANT1_ID)
                .param("time", "10:30")
                .with(userHttpBasic(USER)))
                .andExpect(status().isNoContent());

Restaurant restaurantAfter = restaurantRepository.get(RESTAURANT1_ID);

“restaurantAfter”是真正的对象

“get()”进入存储库:

    @Override
    public Restaurant get(int id) 
        return repository.findById(id).orElse(null);
    

【问题讨论】:

可以用问题格式提问吗? 【参考方案1】:

你在方法或服务类本身上有@Transactional注解吗?

这可以解释观察到的行为。

当在事务中执行方法时,从数据库获取或合并/保存到数据库的实体会被缓存,直到事务结束(通常是方法结束)。这意味着对具有相同 ID 的实体的任何调用都将直接从缓存中返回,并且不会命中数据库。

这里有一些关于 Hibernate 的缓存和代理的文章:

Understanding Hibernate First Level Cache with Example How does a JPA Proxy work and how to unproxy it with Hibernate The best way to initialize LAZY entity and collection proxies with JPA and Hibernate

回到你的例子:

先调用findById(id),然后getOne(id)为两者返回相同的实体对象 先调用getOne(id),然后findById(id)为两者返回相同的代理

那是因为它们共享同一个id 并在同一个事务中执行。

getOne() 上的文档指出它可以返回 an instance 而不是引用 (HibernateProxy),因此可以预期它返回一个实体:

T getOne(ID id)

返回对具有给定标识符的实体的引用。

根据 JPA 持久性提供程序的实现方式,这很可能 总是返回一个实例并抛出一个 EntityNotFoundException 第一次访问。他们中的一些人会立即拒绝无效的标识符。

参数: id - 不能为空。

返回: 对具有给定标识符的实体的引用。

另一方面,findById() 上的文档没有任何提示,它可以返回除实体的Optional 或空的Optional 之外的任何内容:

可选的 findById(ID id)

通过 id 检索实体。

参数:id - 不能为空。

返回:具有给定 id 的实体或 Optional#empty() 如果没有找到

我花了一些时间寻找更好的解释,但没有找到,所以我不确定这是否是 findById() 实现中的错误,或者只是一个没有(充分)记录的功能。

作为解决问题的方法,我可以建议:

    不要在同一事务方法中两次获取同一实体。 :) 在不需要时避免使用@Transactional。交易也可以手动管理。这里有一些关于这个主题的好文章: 5 common Spring @Transactional pitfalls Spring Transactional propagation modes Spring pitfalls: transactional tests considered harmful。 使用其他方法在(重新)加载之前分离第一个加载的实体/代理:
import javax.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@Service
public class SomeServiceImpl implements SomeService 

    private final SomeRepository repository;
    private final EntityManager entityManager;

    // constructor, autowiring

    @Override
    public void someMethod(long id) 
        SomeEntity getOne = repository.getOne(id); // Proxy -> added to cache

        entityManager.detach(getOne); // removes getOne from the cache

        SomeEntity findById = repository.findById(id).get(); // Entity from the DB
    
    类似于第 3 种方法,但不是从缓存中删除单个对象,而是使用 clear() 方法一次性删除所有对象:
import javax.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@Service
public class SomeServiceImpl implements SomeService 

    private final SomeRepository repository;
    private final EntityManager entityManager;

    // constructor, autowiring

    @Override
    public void someMethod(long id) 
        SomeEntity getOne = repository.getOne(id); // Proxy -> added to cache

        entityManager.clear(); // clears the cache

        SomeEntity findById = repository.findById(id).get(); // Entity from the DB
    

相关文章:

When use getOne and findOne methods Spring Data JPA Hibernate Session: evict() and merge() Example clear(), evict() and close() methods in Hibernate JPA - Detaching an Entity Instance from the Persistence Context Difference between getOne and findById in Spring Data JPA?

编辑:

这是一个simple project 演示问题或功能(取决于观点)。

【讨论】:

我认为解释是正确的。不过,针对@Transactional 的建议具有误导性。手动管理的事务也是如此,事务应该由您的事务需求而不是 JPA 的某些怪癖来确定。【参考方案2】:

对 - 已接受 - 答案的一些扩展:

如果你使用 Spring Boot,那么它会自动启用 Open Session In View 过滤器,它基本上作为每个请求的事务。

如果您想关闭此功能,请将以下行添加到 application.properties:

spring.jpa.open-in-view=false

OSIV 从性能和可扩展性的角度来看确实是个坏主意。

【讨论】:

以上是关于为啥“findById()”在同一实体上调用 getOne() 后返回代理?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 FindById 返回 Optional? [复制]

为啥不能修改 Mongoose 查询返回的数据(例如:findById)

为啥 Hibernate 在一对多双向更新操作中给出同一实体的多个表示?

杰克逊:当调用不同的 Rest EndPoint 时,同一实体上的多个序列化器

在 PHP 中,为啥可以在同一类类型的方法中创建的新实例上调用私有方法? [复制]

为啥 mongoose 的 deleteOne 和 findById 对已删除的 id 起作用