当使用getOne和findOne方法时Spring Data JPA


154

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

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

观察@Transactionalhas具有Propagation.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接口扩展自CrudRepository。该findOne(id)方法在中定义CrudRepository

我的问题是:

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

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

根据getOne API:

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

根据findOne API:

通过其ID检索实体。

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

4)建议使用哪种方法?

提前致谢。


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

Answers:


136

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);

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

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

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

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

Optional<T> findById(ID id); 

现在它返回一个Optional。可以预防的还不错NullPointerException

因此,实际的选择是在Optional<T> findById(ID id)和之间T getOne(ID id)


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

1)Optional<T> 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声明(强调是我的):

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

实际上,参考术语实际上是board,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);

幸运的是,EntityManagerjavadoc更好地定义了它的意图(强调是我的):

获取一个实例,其状态可能会延迟获取。如果请求的实例在数据库中不存在,则在首次访问实例状态时抛出EntityNotFoundException 。(调用getReference时,允许持久性提供程序运行时引发EntityNotFoundException。)应用程序不应期望实例状态在分离后可用,除非在打开实体管理器时由应用程序访问了实例状态

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

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

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


为什么API如此不清楚?

最后,向Spring-Data-JPA开发人员提出两个问题:

  • 为什么没有更清晰的文档getOne()呢?实体延迟加载实际上不是一个细节。

  • 为什么需要介绍getOne()包装EM.getReference()
    为什么不直接粘在包装方法:getReference()?这种EM方法在getOne() 传送如此简单的过程时确实非常特别。


3
我很困惑,为什么getOne()没有引发EntityNotFoundException,但是您的“在首次访问实例状态时引发EntityNotFoundException”向我解释了这个概念。谢谢
TheCoder

此答案的摘要:getOne()使用延迟加载,EntityNotFoundException如果未找到任何项目,则抛出。findById()立即加载,如果未找到则返回null。由于getOne()存在一些不可预测的情况,因此建议改用findById()。
Janac Meena

124

基本区别在于,它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!!!
}

1
延迟加载不意味着仅在使用实体时才会加载吗?因此,我希望getEnt为null,如果不执行第二个代码,则它为第二个。请您解释一下。谢谢!
Doug

如果包装在CompletableFuture <> Web服务中,我会发现由于它的懒惰实现,您将要使用findOne()和getOne()。
Fratt

76

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

请参阅文档中的本节。您覆盖已经就位的事务可能会导致此问题。但是,如果没有更多信息,则很难回答。

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

在不深入研究Spring Data JPA的内部的情况下,差异似乎在于用于检索实体的机制。

如果您在另请参见下的JavaDoc查看getOne(ID)

See Also:
EntityManager.getReference(Class, Object)

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

然而,该文档findOne(ID)没有提到这一点。

线索也在存储库的名称中。 JpaRepository是JPA专用的,因此可以根据需要将调用委派给实体管理器。 CrudRepository与所使用的持久性技术无关。看这里。它用作JPA,Neo4J等多种持久性技术的标记接口。

因此,这两种方法在您的用例中并没有真正的“差异”,只是findOne(ID)通用性比专业性更高getOne(ID)。您使用哪一个取决于您和您的项目,但是我个人会坚持使用,findOne(ID)因为它会使您的代码不那么依赖于实现,并且将来无需进行过多重构即可打开迁移到MongoDB等之类的大门:)


谢谢多诺万,有你的答复。
曼努埃尔·乔丹

20
我认为there's not really a 'difference' in the two methods在这里说这是非常令人误解的,因为在检索实体的方式以及方法应返回的内容方面确实存在很大差异。@davidxxx进一步给出的答案很好地说明了这一点,我认为使用Spring Data JPA的每个人都应该意识到这一点。否则,可能会引起很多头痛。
fridberg

16

getOne方法仅返回来自数据库的引用(延迟加载)。因此,基本上您不在事务之内(Transactional不考虑您已在服务类中声明的内容),并且会发生错误。


似乎EntityManager.getReference(Class,Object)返回“ nothing”,因为我们在新的Transaction范围内。
曼努埃尔·乔丹

2

从以上答案中我真的很难。从调试的角度来看,我几乎花了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始终为null。更改为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

}


-1

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


-2

我有一个类似的问题,理解为什么JpaRespository.getOne(id)无法正常工作并引发错误。

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

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


不幸的是,这没有提供并回答问题,也没有改善现有的答案。
JSTL

我知道了,没问题。
akshaymittal143
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.