【问题标题】:When use getOne and findOne methods Spring Data JPA使用 getOne 和 findOne 方法时 Spring Data JPA
【发布时间】:2018-08-25 10:03:15
【问题描述】:

我有一个用例,它调用以下内容:

@Override
@Transactional(propagation=Propagation.REQUIRES_NEW)
public UserControl getUserControlById(Integer id){
    return this.userControlRepository.getOne(id);
}

注意@TransactionalPropagation.REQUIRES_NEW 并且存储库使用 getOne。当我运行该应用程序时,我收到以下错误消息:

Exception in thread "main" org.hibernate.LazyInitializationException: 
could not initialize proxy - no Session
...

但如果我将getOne(id) 更改为findOne(id) 一切正常。

顺便说一句,就在用例调用 getUserControlById 方法之前,它已经调用了 insertUserControl 方法

@Override
@Transactional(propagation=Propagation.REQUIRES_NEW)
public UserControl insertUserControl(UserControl userControl) {
    return this.userControlRepository.save(userControl);
}

这两种方法都是Propagation.REQUIRES_NEW,因为我正在做一个简单的审计控制。

我使用getOne 方法,因为它是在JpaRepository 接口中定义的,并且我的Repository 接口从那里扩展,当然我正在使用JPA。

JpaRepository 接口扩展自CrudRepositoryfindOne(id) 方法在 CrudRepository 中定义。

我的问题是:

  1. 为什么getOne(id) 方法会失败?
  2. 什么时候应该使用getOne(id) 方法?

我正在使用其他存储库,并且都使用 getOne(id) 方法并且一切正常,只有当我使用 Propagation.REQUIRES_NEW 时它才会失败。

根据 getOne API:

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

根据 findOne API:

通过 id 检索实体。

  1. 什么时候应该使用findOne(id) 方法?

  2. 推荐使用什么方法?

【问题讨论】:

  • 您尤其不应该使用 getOne() 来测试数据库中是否存在对象,因为使用 getOne 您总是得到一个对象 != null,而 findOne 提供的是 null。

标签: jpa spring-data spring-data-jpa


【解决方案1】:

1.为什么 getOne(id) 方法会失败?

请参阅此部分in the docs。您覆盖已经存在的事务可能会导致问题。但是,如果没有更多信息,这个问题很难回答。

2。什么时候应该使用 getOne(id) 方法?

如果不深入研究 Spring Data JPA 的内部结构,不同之处似乎在于用于检索实体的机制。

如果您在另请参阅下查看JavaDoc for getOne(ID)

See Also:
EntityManager.getReference(Class, Object)

似乎这个方法只是委托给 JPA 实体管理器的实现。

但是,docsfindOne(ID) 没有提及这一点。

线索也在存储库的名称中。 JpaRepository 是特定于 JPA 的,因此可以在需要时将调用委托给实体管理器。 CrudRepository 与所使用的持久性技术无关。 Look here。它被用作 JPA、Neo4J 等多种持久性技术的标记接口。

因此,对于您的用例,这两种方法并没有真正的“区别”,只是findOne(ID) 比更专业的getOne(ID) 更通用。您使用哪一个取决于您和您的项目,但我个人会坚持使用findOne(ID),因为它使您的代码不那么具体的实现,并为将来迁移到诸如 MongoDB 等之类的东西打开了大门,而无需太多重构:)

【讨论】:

  • 我认为在这里说there's not really a 'difference' in the two methods 非常具有误导性,因为实体的检索方式和您应该期望方法返回的内容确实存在很大差异。 @davidxxx 的回答很好地强调了这一点,我认为每个使用 Spring Data JPA 的人都应该意识到这一点。否则,它可能会导致相当头痛。
【解决方案2】:

getOne 方法仅返回来自 DB 的引用(延迟加载)。 所以基本上你在事务之外(不考虑你在服务类中声明的Transactional),并且发生错误。

【讨论】:

  • 似乎 EntityManager.getReference(Class, Object) 返回“无”,因为我们处于新的事务范围内。
【解决方案3】:

基本区别在于getOne 是延迟加载的,findOne 不是。

考虑以下示例:

public static String NON_EXISTING_ID = -1;
...
MyEntity getEnt = myEntityRepository.getOne(NON_EXISTING_ID);
MyEntity findEnt = myEntityRepository.findOne(NON_EXISTING_ID);

if(findEnt != null) {
     findEnt.getText(); // findEnt is null - this code is not executed
}

if(getEnt != null) {
     getEnt.getText(); // Throws exception - no data found, BUT getEnt is not null!!!
}

【讨论】:

  • doesntlazy加载是指只有实体要使用的时候才会加载?所以我希望 getEnt 为空,如果不执行第二个代码,请您解释一下。谢谢!
【解决方案4】:

TL;DR

T findOne(ID id)(旧 API 中的名称)/Optional<T> findById(ID id)(新 API 中的名称)依赖于执行实体预加载EntityManager.find()

T getOne(ID id) 依赖于执行实体延迟加载EntityManager.getReference()。所以为了保证实体的有效加载,需要对其调用方法。

findOne()/findById()确实比getOne()更清晰易用。
所以在大多数情况下,支持findOne()/findById() 而不是getOne()


API 变更

至少从2.0版本,Spring-Data-Jpa修改findOne()
之前在CrudRepository接口中定义为:

T findOne(ID primaryKey);

现在,您将在 CrudRepository 中找到的单个 findOne() 方法在 QueryByExampleExecutor 接口中定义为:

<S extends T> Optional<S> findOne(Example<S> example);

最终由SimpleJpaRepository 实现,CrudRepository 接口的默认实现。
此方法是通过示例搜索进行的查询,您不希望将其作为替换。

实际上,具有相同行为的方法仍然存在于新 API 中,但方法名称已更改。
CrudRepository接口中,它从findOne()重命名为findById()

Optional<T> findById(ID id); 

现在它返回一个Optional。防止NullPointerException这还不错。

所以,现在实际的选择是在Optional&lt;T&gt; findById(ID id)T getOne(ID id) 之间。


两个不同的方法依赖于两个不同的 JPA EntityManager 检索方法

1) Optional&lt;T&gt; findById(ID id) javadoc 声明它:

通过 id 检索实体。

当我们研究实现时,我们可以看到它依赖于EntityManager.find() 来进行检索:

public Optional<T> findById(ID id) {

    Assert.notNull(id, ID_MUST_NOT_BE_NULL);

    Class<T> domainType = getDomainClass();

    if (metadata == null) {
        return Optional.ofNullable(em.find(domainType, id));
    }

    LockModeType type = metadata.getLockModeType();

    Map<String, Object> hints = getQueryHints().withFetchGraphs(em).asMap();

    return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}

这里em.find() 是一个EntityManager 方法,声明为:

public <T> T find(Class<T> entityClass, Object primaryKey,
                  Map<String, Object> properties);

它的 javadoc 状态:

使用指定属性按主键查找

因此,检索加载的实体似乎是预期的。

2) 虽然T getOne(ID id) javadoc 声明(重点是我的):

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

事实上,reference 术语确实是板子,JPA API 没有指定任何getOne() 方法。
因此,了解 Spring 包装器所做的最好的事情是研究实现:

@Override
public T getOne(ID id) {
    Assert.notNull(id, ID_MUST_NOT_BE_NULL);
    return em.getReference(getDomainClass(), id);
}

这里em.getReference() 是一个EntityManager 方法,声明为:

public <T> T getReference(Class<T> entityClass,
                              Object primaryKey);

幸运的是,EntityManager javadoc 更好地定义了它的意图(重点是我的):

获取一个实例,其状态可能被延迟获取。如果要求 数据库中不存在实例,EntityNotFoundException 在第一次访问实例状态时抛出。 (坚持 允许提供程序运行时抛出 EntityNotFoundException 当 getReference 被调用时。)应用程序不应该期望 实例状态将在分离时可用,除非它是 实体管理器打开时由应用程序访问。

因此,调用 getOne() 可能会返回一个延迟获取的实体。
这里,惰性获取不是指实体的关系,而是指实体本身。

这意味着如果我们调用getOne()然后关闭Persistence上下文,实体可能永远不会被加载,所以结果真的是不可预测的。
例如,如果代理对象被序列化,您可以获得null 引用作为序列化结果,或者如果在代理对象上调用方法,则会引发诸如LazyInitializationException 之类的异常。
所以在这种情况下,EntityNotFoundException 的抛出是使用getOne() 处理数据库中不存在的实例的主要原因,因为在实体不存在时可能永远不会执行错误情况。

无论如何,为了确保它的加载,您必须在会话打开时操作实体。您可以通过调用实体上的任何方法来实现。
或者使用findById(ID id) 代替更好的替代方法。


为什么 API 这么不清楚?

最后,给 Spring-Data-JPA 开发人员的两个问题:

  • 为什么没有更清晰的 getOne() 文档?实体懒加载真的不是细节。

  • 为什么要引入getOne()来包裹EM.getReference()
    为什么不简单地坚持包装方法 :getReference() ? 这个 EM 方法真的很特别,getOne() 传达了一个如此简单的处理。

【讨论】:

  • 我很困惑为什么 getOne() 没有抛出 EntityNotFoundException,但是你的“第一次访问实例状态时抛出 EntityNotFoundException”向我解释了这个概念。谢谢
  • 这个答案的总结:getOne() 使用延迟加载,如果没有找到项目,则抛出 EntityNotFoundExceptionfindById() 立即加载,如果未找到则返回 null。由于 getOne() 存在一些不可预知的情况,因此建议使用 findById() 代替。
【解决方案5】:

从上面的答案中我真的觉得很难。 从调试的角度来看,我几乎花了 8 个小时才知道这个愚蠢的错误。

我正在测试 spring+hibernate+dozer+Mysql 项目。说清楚。

我有用户实体,书籍实体。你做映射的计算。

多本书是否与一个用户绑定。但在 UserServiceImpl 中,我试图通过 getOne(userId);

找到它
public UserDTO getById(int userId) throws Exception {

    final User user = userDao.getOne(userId);

    if (user == null) {
        throw new ServiceException("User not found", HttpStatus.NOT_FOUND);
    }
    userDto = mapEntityToDto.transformBO(user, UserDTO.class);

    return userDto;
}

剩下的结果是

{
"collection": {
    "version": "1.0",
    "data": {
        "id": 1,
        "name": "TEST_ME",
        "bookList": null
    },
    "error": null,
    "statusCode": 200
},
"booleanStatus": null

}

上面的代码没有获取用户正在阅读的书籍。

由于 getOne(ID),bookList 始终为空。更改为 findOne(ID) 后。结果是

{
"collection": {
    "version": "1.0",
    "data": {
        "id": 0,
        "name": "Annama",
        "bookList": [
            {
                "id": 2,
                "book_no": "The karma of searching",
            }
        ]
    },
    "error": null,
    "statusCode": 200
},
"booleanStatus": null

}

【讨论】:

    【解决方案6】:

    我在理解为什么 JpaRespository.getOne(id) 不起作用并引发错误时遇到了类似的问题。

    我去并更改为 JpaRespository.findById(id),它要求您返回一个 Optional。

    这可能是我对 StackOverflow 的第一条评论。

    【讨论】:

    • 很遗憾,这并没有提供和回答问题,也没有改进现有的答案。
    • 我明白了,没问题。
    【解决方案7】:

    虽然 spring.jpa.open-in-view 为 true,但我对 getOne 没有任何问题,但将其设置为 false 后,我得到了 LazyInitializationException。然后通过替换 findById 解决了问题。
    尽管有另一种解决方案而不替换 getOne 方法,那就是将@Transactional 放在调用repository.getOne(id) 的方法中。这样,事务将存在,会话不会在您的方法中关闭,并且在使用实体时不会出现任何 LazyInitializationException。

    【讨论】:

      猜你喜欢
      • 2018-08-11
      • 1970-01-01
      • 1970-01-01
      • 2021-03-07
      • 2018-08-25
      • 1970-01-01
      • 2018-02-26
      相关资源
      最近更新 更多