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

188

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

@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 接口是从 CrudRepository 扩展来的。 findOne(id) 方法在 CrudRepository 中定义。

我的问题是:

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

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

根据 getOne API 的说明:

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

根据 findOne API 的说明:

按其 id 检索实体。

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

  2. 推荐使用哪种方法?


你特别不应该使用getOne()来测试数据中对象的存在性,因为使用getOne()总是会返回一个非空对象,而findOne()则会返回null。 - Uwe Allner
7个回答

202

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中,具有相同行为的方法仍然存在,但方法名称已更改。
它从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(我强调)声明如下:

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

实际上,引用术语确实很广泛,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异常。(当调用getReference时,持久性提供程序运行时允许抛出EntityNotFoundException异常。)除非应用程序在实体管理器打开时访问它,否则应用程序不应期望实例状态在分离后可用。
因此,调用getOne()可能会返回一个延迟获取的实体。 这里,延迟获取并不是指实体的关系,而是指实体本身。
这意味着,如果我们调用getOne(),然后关闭持久性上下文,那么该实体可能永远不会被加载,因此结果是不可预测的。 例如,如果代理对象被序列化,您可能会得到一个空引用作为序列化结果,或者如果在代理对象上调用方法,则会抛出诸如LazyInitializationException之类的异常。 因此,在这种情况下,抛出EntityNotFoundException的情况(这是使用getOne()处理数据库中不存在的实例的主要原因)可能永远不会发生。
无论如何,为了确保其加载,您必须在会话打开时操作实体。您可以通过调用实体上的任何方法来实现这一点。 或者更好的选择是使用findById(ID id)
为什么API如此不清晰?
最后,对于Spring-Data-JPA开发人员的两个问题:
1.为什么没有更清晰的getOne()文档?实体的延迟加载确实不是一个细节。
2.为什么需要引入getOne()来包装EM.getReference()? 为什么不简单地坚持包装方法:getReference()? 这个EM方法真的非常特殊,而getOne()传达了一个如此简单的处理。

6
我曾经困惑为什么getOne()没有抛出EntityNotFoundException,但你的“当首次访问实例状态时会抛出EntityNotFoundException”解释了这个概念。谢谢。 - The Coder
4
这个回答的总结如下:getOne() 使用懒加载,如果没有找到项则会抛出 EntityNotFoundException 异常。 findById() 立即加载并返回 null 如果未找到。由于 getOne() 存在一些不可预测的情况,建议改用 findById() - Janac Meena
从Spring Data JPA 2.5开始,getOne()已被弃用,改为新命名的getById()(只是将返回实体的引用的getOne()重命名)。我建议使用findById()以避免意外异常。 - Brice Roncace

130

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

2
懒加载的意思是只有在实体被使用时才会被加载,因此我期望getEnt为null,并且第二个if语句中的代码不会被执行。能否请您解释一下。谢谢! - Doug
如果包装在CompletableFuture<> Web服务中,我发现由于其惰性实现,您将希望使用findOne()而不是getOne()。 - Fratt

77

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

请参考文档中的这一部分。覆盖已有的事务可能导致问题,但没有更多的信息很难回答。

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

不深入研究Spring Data JPA的内部机制,两者之间的区别似乎在于检索实体所使用的机制。

如果您查看getOne(ID)JavaDoc,请参阅“另请参见”:

See Also:
EntityManager.getReference(Class, Object)

看起来这个方法只是委托给JPA实体管理器的实现。

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

仓库名称中的线索也很明显。JpaRepository是特定于JPA的,因此如果需要,可以将调用委托给实体管理器。CrudRepository对使用的持久化技术不加区分。 看这里。 它被用作多种持久化技术(如JPA,Neo4J等)的标记接口。

因此,对于您的用例,这两种方法并没有真正的“区别”,只是findOne(ID)比更专业的getOne(ID)更通用。 您使用哪种取决于您和您的项目,但我个人会坚持使用findOne(ID),因为它使您的代码 less implementation specific,并为以后转移到诸如MongoDB之类的东西打开了大门,而无需进行太多的重构 :)


22
我认为说“这两种方法并没有什么不同”非常误导人,因为在实体的检索方式和方法返回值方面确实存在很大的差异。@davidxxx 在下面的回答中很好地强调了这一点,我认为所有使用 Spring Data JPA 的人都应该意识到这一点,否则可能会引起很多麻烦。 - fridberg

16

getOne方法仅返回来自数据库的引用(延迟加载)。 基本上你在事务之外(服务类中声明的Transactional不被视为),因此会出现错误。


似乎由于我们处于新的事务范围内,EntityManager.getReference(Class, Object)返回“空”。 - Manuel Jordan

2

我发现上面的回答很难理解。从调试的角度来看,我花了将近8个小时才知道这个愚蠢的错误。

我有一个测试spring+hibernate+dozer+Mysql项目。为了清楚明白。

我有User实体和Book实体。你需要计算映射。

多本书与一个用户相关联。但是在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;
}

Rest结果如下:
{
"collection": {
    "version": "1.0",
    "data": {
        "id": 1,
        "name": "TEST_ME",
        "bookList": null
    },
    "error": null,
    "statusCode": 200
},
"booleanStatus": null

以上代码没有获取用户阅读的书籍。

bookList一直是null,因为使用了getOne(ID)。更改为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为true时,我使用getOne没有任何问题,但是在将其设置为false后,我遇到了LazyInitializationException。然后通过替换为findById解决了这个问题。
虽然有另一种解决方案,而不是替换getOne方法,那就是在调用repository.getOne(id)的方法上加上@Transactional。这样事务将存在,并且会话不会在您的方法中关闭,使用实体时也不会出现LazyInitializationException。


-3
我曾经也遇到过类似的问题,不理解为什么 JpaRespository.getOne(id) 不能正常工作并且会抛出错误。
后来我使用了 JpaRespository.findById(id),需要你返回一个 Optional 类型的值。
这应该是我在 StackOverflow 上发表的第一条评论。

1
不幸的是,这并没有回答问题,也没有改进现有的答案。 - JSTL
我明白了,没问题。 - akshaymittal143

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接