休眠 OnetoMany 与 Fetch Lazy 给 LazyInitializationException

Posted

技术标签:

【中文标题】休眠 OnetoMany 与 Fetch Lazy 给 LazyInitializationException【英文标题】:Hibernate OnetoMany with Fetch Lazy giving LazyInitializationException 【发布时间】:2021-08-21 05:50:44 【问题描述】:

我是 Java Persistence API 和 Hibernate 的新手,我使用 Spring JPA 存储库在 DB 中进行查询。现在我在 Parent Child 关系中有两个实体,Parent 实体与 @OneToMany 和 Child 实体与 @ManyToOne 映射。

父实体:-

@Entity
@Table(name = "PERSONS")
public class Persons 

    ...

    @OneToMany(mappedBy = "person", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
    public List<Cards> cards = new ArrayList<Cards>();

    ...

子实体:-

@Entity
@Table(name = "CARDS")
public class Cards 

    ...

    @ToString.Exclude
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "PERSON_ID", nullable = false, insertable = false, updatable = false)
    public Person person;
    ...

我正在使用我的 PersonsRepository,如下所示:-

@Repository
public interface PersonsRepository extends JpaRepository<Persons, String> 

     ....

现在关系中使用的 fetchType 在两端都是 LAZY。现在,每当我尝试遍历列表并尝试使用 person.getCards() 处理每个卡片时,都会出现以下错误:-

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.xxx.abc.Persons.cards, could not initialize proxy - no Session
    at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:606)
    at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:218)
    at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:585)
    at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:149)
    at org.hibernate.collection.internal.PersistentSet.iterator(PersistentSet.java:188)
    at java.util.Spliterators$IteratorSpliterator.estimateSize(Spliterators.java:1821)
    at java.util.Spliterator.getExactSizeIfKnown(Spliterator.java:408)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472)
    at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)

现在我发现每个人都说在 Hibernate 中使用 LAZY 是最好的方法,而且它还更多地说明了代码的正确设计。我同意我使用 person.getCards() 不会有任何打开的会话,这就是它给我 LazyInitializationException 的原因,但这背后的意图是保存更多的数据库调用。

假设我有 1000 个人员列表,这意味着我必须为每个人进行 1000 次单独调用 getCards()。这就是为什么如果我在 Person @OneToMany 中使用 FETCHTYPE.EAGER,性能影响是什么,因为所有内容都会被急切地获取。

需要有关针对此类问题所遵循的最佳做法的建议。 TIA。

编辑:- 我在服务类中有一个方法,我在其中使用@transactional,如下所示:-

@Transactional(readOnly = true)
    public void fetchData(Integer param1, Timestamp param2, Timestamp param3, List<String> param4, NavigableMap<Long, List<Cards>> param5) 
        List<Persons> validPersons = personRepo.getCardsPerPerson(param2, param3);
        if(validPersons != null && !validPersons.isEmpty()) 
            // store the cards on the basis of epoch timestamp
            prepareTimestampVsCardsMap(validPersons, param4, param5);
        
    

private void prepareTimestampVsCardsMap(List<Persons> validPersons, List<String> uList, NavigableMap<Long, List<Cards>> timestampVsCardsList) 
        for(Person person : validPersons) 
            Long epoch = order.getOrderTime().getTime();
            Set<Cards> cardsPerPerson = person.getCards();


此外,存储库中用于获取与某人关联的卡片的查询正在使用 join fetch,如下所示:-

@Query(value = "select p from Person p join fetch Cards c on p.id = c.id WHERE p.orderTime BETWEEN ?1 AND ?2 ORDER BY orderTime ASC")
    public List<Person> getCardsPerPerson(Timestamp param1, Timestamp param2);

我仍然遇到上面提到的 LazyInitializationException。谁能帮忙。

【问题讨论】:

你在哪里调用 person.getCards()?在服务类中对吗?我假设您没有在您的服务方法中使用“@Transactional”,在这种情况下,当您从存储库中获取人员时,会话将关闭并且您的延迟初始化如果没有用。您的服务方法上应该有“@Transactional”,以便在您的所有业务案例在单个事务中完成后关闭会话。 最好将获取策略设置为Lazy。如果你需要卡片和人,你应该通过你的存储库调用来获取它(例如,在方法上使用@Query("select p from Person p join fetch Cards c on p.id = c.person.id"),或者使用实体图)。这样,您只需调用一次 DB 即可获取您需要的所有数据。 @Lucia 请看我的编辑。我已经添加了所有东西并使用“@Transactional”但得到了同样的错误 @StefanGolubović 我以同样的方式使用了查询,使用 join fetch 来获取与某人关联的卡片列表。请在我的编辑中查看。仍然没有成功并得到同样的异常 @user2594 抱歉,我混合使用了 SQL 和 JPQL。你可以试试@Query("select p from Person p join fetch p.cards where ...")。您总是可以在 vladmihalcea.com 博客上找到解决持久性问题的方法。 【参考方案1】:

您误以为EAGER 加载意味着Hibernate 将使用一条语句获取所有数据,这是错误的。使用EAGER 作为策略,框架将只执行获取每个实体的所有数据所需的每个查询。

示例:如果一个实体有 2 个EAGER 关系,则获取一个将导致 3 个语句,一个用于加载实体,一个用于其每个关系。如果您有 3 个实体,您将有 7 个语句,初始语句加载 3 个对象,每个对象加上 2 个。

当您的治疗需要一切时,目前没有真正的性能影响。但大多数应用程序不是由一种治疗方法制成的。这意味着您的应用程序中的每个处理都将加载 EAGER 的所有内容,即使不需要。这将有效地减慢一切。如果所有内容都在 EAGER 中,您也有将所有数据库加载到内存中的风险。

这就是为什么LAZY 是推荐的方法。

至于您的LazyInitializationException,在您的堆栈跟踪中似乎您正在使用stream API。由于缺少细节,这是一个疯狂的猜测,但 JPA/Hibernate 不处理线程之间的共享会话,因此如果您使用parrallelStream,它可能会导致问题。

【讨论】:

【参考方案2】:

首先,使用FetchType.LAZY 而不是FetchType.EAGER 总是更好。为什么?因为您可能并不每次都需要所有数据。如果您想返回Persons 的列表并以某种方式在某处显示它们,您是否还需要获取他们所有的卡片?如果没有,那么FetchType.LAZY 将是更好的选择,然后您可以控制需要多少数据。

LazyInitializationException 通常表示您在打开Session 时没有获取所需的所有数据。获取相关数据的方法有很多种(在处理请求时没有一种方法可以保持 Session 处于打开状态):

1。在您的 JPQL/HQL 中使用 join fetch

@Query("select p from Person p join fetch p.cards where ...")
List<Person> getCardsPerPerson(Timestamp param1, Timestamp param2);

2。如果您使用的是 Spring Data,则可以使用 @EntityGraph 而不是 join fetch

@EntityGraph(attributePaths =  "cards" )
List<Person> getPersons();

这样,每次你调用getPersons,它也会获取卡片。当然,如果一定要写@Query,就不能用这个了。

如果您将Spring Data's naming conventions 用于一些简单的查询,那么@EntityGraph 将是获取关联的一个选项。

3。使用Criteria API

同样,如果您使用的是 Spring Data,这只是一个备用解决方案,以防您最终得到 MultipleBagFetchException。这个我就不赘述了,如果你遇到这个异常,你会在Vlad Mihalcea的博文The best way to fix the Hibernate MultipleBagFetchException找到解决办法。

【讨论】:

以上是关于休眠 OnetoMany 与 Fetch Lazy 给 LazyInitializationException的主要内容,如果未能解决你的问题,请参考以下文章

休眠 HQL 到条件

忽略条件中的休眠获取

休眠删除父级

休眠更新到 OneToMany 集合的唯一约束冲突

为啥加载惰性集合

@Batchsize 注释不适用于 OneToMany