为啥“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() 后返回代理?的主要内容,如果未能解决你的问题,请参考以下文章
为啥不能修改 Mongoose 查询返回的数据(例如:findById)
为啥 Hibernate 在一对多双向更新操作中给出同一实体的多个表示?
杰克逊:当调用不同的 Rest EndPoint 时,同一实体上的多个序列化器