ChatGPT解决这个技术问题 Extra ChatGPT

使用 getOne 和 findOne 方法时 Spring Data JPA

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

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

观察 @Transactional 具有 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 中定义。

我的问题是:

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

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

根据 getOne API:

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

根据 findOne API:

通过 id 检索实体。

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

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

d
davidxxx

TL;博士

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

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

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

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 状态(重点是我的):

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

事实上,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。 (当调用 getReference 时,允许持久性提供程序运行时抛出 EntityNotFoundException。)应用程序不应期望实例状态在分离时可用,除非它在实体管理器打开时由应用程序访问。

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

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

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

为什么 API 这么不清楚?

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

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

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


我很困惑为什么 getOne() 没有抛出 EntityNotFoundException,但是你的“第一次访问实例状态时抛出 EntityNotFoundException”向我解释了这个概念。谢谢
此答案的摘要:getOne() 使用延迟加载,如果未找到任何项目,则抛出 EntityNotFoundExceptionfindById() 立即加载,如果未找到则返回 null。由于 getOne() 存在一些不可预知的情况,因此建议使用 findById() 代替。
从 Spring Data JPA 2.5 开始,getOne() 已被弃用,取而代之的是新命名的 getById()(只是 getOne() 的重命名,返回了对实体的 reference,如上所述)。我建议使用 findById() 来避免意外异常。
a
andyvn22

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

不延迟加载意味着它只会在要使用实体时加载?所以我希望 getEnt 为空,如果不执行,第二个里面的代码你能解释一下吗?谢谢!
如果包裹在 CompletableFuture<> Web 服务中,我发现你会想要使用 findOne() 与 getOne() ,因为它是惰性实现。
P
Peter Hall

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

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

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

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

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

See Also:
EntityManager.getReference(Class, Object)

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

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

线索也在存储库的名称中。 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 的人都应该意识到这一点。否则,它会引起相当大的头痛。
n
naXa stands with Ukraine

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


似乎 EntityManager.getReference(Class, Object) 返回“无”,因为我们处于新的事务范围内。
E
EngineSense

从上面的答案中,我真的觉得很难。从调试的角度来看,我几乎花了 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

}


F
Farshad Falaki

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


a
akshaymittal143

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

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

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


不幸的是,这并没有提供和回答问题,也没有改进现有的答案。
我明白了,没问题。