休眠条件使用FetchType.EAGER多次返回子级


115

我有一个Order包含列表的类,并使用OrderTransactions一对多的Hibernate映射将其映射,如下所示:

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public List<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}

这些Order还具有一个field orderStatus,用于根据以下条件进行过滤:

public List<Order> getOrderForProduct(OrderFilter orderFilter) {
    Criteria criteria = getHibernateSession()
            .createCriteria(Order.class)
            .add(Restrictions.in("orderStatus", orderFilter.getStatusesToShow()));
    return criteria.list();
}

这可以正常工作,并且结果符合预期。

现在是我的问题:为什么当我将访存类型显式设置为时EAGEROrders在结果列表中出现多次?

@OneToMany(targetEntity = OrderTransaction.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
public List<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}

我如何更改我的标准代码才能在新设置下达到相同的结果?


1
您是否尝试启用show_sql以查看其下发生了什么?
Mirko N.

请同时添加OrderTransaction和Order类代码。\
Eran Medan 2010年

Answers:


115

如果我正确理解您的配置,这实际上是预期的行为。

Order在任何结果中都得到相同的实例,但是由于您现在正在使用进行联接OrderTransaction,因此它必须返回与常规sql联接将返回的结果相同的结果量

因此,实际上它应该出现多次。这是由作者(是Gavin King)自己解释得非常好这里:它既解释了为什么,以及如何仍然得到不同的结果


在Hibernate FAQ中也提到过:

Hibernate不会为启用了集合的外部联接获取的查询返回不同的结果(即使我使用了distinct关键字)?首先,您需要了解SQL以及OUTER JOIN在SQL中的工作方式。如果您不完全理解并理解SQL中的外部联接,请不要继续阅读此FAQ项,而请查阅SQL手册或教程。否则,您将无法理解以下解释,并且会在Hibernate论坛上抱怨此行为。

可能返回相同Order对象的重复引用的典型示例:

List result = session.createCriteria(Order.class)
                    .setFetchMode("lineItems", FetchMode.JOIN)
                    .list();

<class name="Order">
    ...
    <set name="lineItems" fetch="join">

List result = session.createCriteria(Order.class)
                       .list();
List result = session.createQuery("select o from Order o left join fetch o.lineItems").list();

所有这些示例都产生相同的SQL语句:

SELECT o.*, l.* from ORDER o LEFT OUTER JOIN LINE_ITEMS l ON o.ID = l.ORDER_ID

想知道为什么有重复吗?查看SQL结果集,Hibernate不会在外部联接结果的左侧隐藏这些重复项,而是返回驱动表的所有重复项。如果数据库中有5个订单,并且每个订单有3个订单项,则结果集将为15行。这些查询的Java结果列表将包含15个元素,所有元素均为Order类型。Hibernate将仅创建5个Order实例,但是SQL结果集的重复项将保留为这5个实例的重复引用。如果您不明白这最后一句话,则需要阅读Java以及Java堆上的实例与对该实例的引用之间的区别。

(为什么选择左外部联接?如果您有没有订单项的其他订单,则结果集将是16行,右侧填充NULL,而订单项数据用于其他订单。即使您想要订单,即使他们没有订单项,对吗?如果没有,请在您的HQL中使用内部联接提取)。

默认情况下,Hibernate不会过滤掉这些重复的引用。有些人(不是您)实际上想要这个。如何过滤掉它们?

像这样:

Collection result = new LinkedHashSet( session.create*(...).list() );

121
即使您理解以下解释,您也可能会在Hibernate论坛上抱怨这种行为,因为它正在转变愚蠢的行为!
汤姆·安德森

17
汤姆说得很对,我忘记了加文·金斯的傲慢态度。他还说:“默认情况下,Hibernate不会过滤掉这些重复的引用。有些人(而不是您)实际上想要这个'当人们实际蚂蚁对此感兴趣。
保罗·泰勒

16
@TomAnderson是的。为什么有人需要这些副本?我出于纯粹的好奇心询问,因为我不知道...您可以自己创建副本,您可以根据需要创建尽可能多的副本.. ;-)
Parobay 2014年

13
叹。恕我直言,这实际上是Hibernate的缺陷。我想优化查询,因此我在映射文件中从“选择”转到“加入”。突然,我的代码到处都是。然后,我跑来跑去并通过添加结果转换器和其他方法修复所有DAO。用户体验==非常负面。我知道有些人出于奇怪的原因绝对喜欢重复,但是为什么我不能通过指定fetch =“ justworkplease”说“更快地获取这些对象,但不要让我烦恼重复”?
Roman Zenka'Mar

@Eran:我正面临类似的问题。我没有得到重复的父对象,但是我使每个父对象中的子对象重复的次数与响应中父对象的数量一样多。知道为什么会出现这个问题吗?
mantri

93

除了Eran提到的以外,获得所需行为的另一种方法是设置结果转换器:

criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

8
这在大多数情况下都适用。...,除非您尝试使用“标准”来访存2个集合/关联。
JamesD 2012年

42

尝试

@Fetch (FetchMode.SELECT) 

例如

@OneToMany(targetEntity = OrderTransaction.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Fetch (FetchMode.SELECT)
public List<OrderTransaction> getOrderTransactions() {
return orderTransactions;

}


11
FetchMode.SELECT增加了Hibernate触发的SQL查询的数量,但确保每个根实体记录仅一个实例。在这种情况下,Hibernate将为每个子记录触发一个选择。因此,您应该考虑性能方面的因素。
Bipul 2014年

1
@BipulKumar是的,但是当我们不能使用延迟获取时,这是一个选项,因为我们需要维护一个会话来进行延迟获取,以访问子对象。
mathi 2014年

18

不要使用List和ArrayList,而要使用Set和HashSet。

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public Set<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}

2
这是偶然提到的Hibernate最佳实践还是与OP中的多孩子检索问题有关?
Jacob Zwiers 2014年


得到它了。次要问题。但是,dzone的文章可能应该带有一点盐味...基于作者自己在评论中的承认。
Jacob Zwiers 2014年

2
IMO这是一个很好的答案。如果您不希望重复,那么您很可能宁愿使用Set而不是List-使用Set(当然实现正确的equals / hascode方法)可以为我解决问题。如redhat doc所述,在实现哈希码/等于时要当心,不要使用id字段。
2016年

1
感谢您的IMO。而且,创建equals()和hashCode()方法也不会遇到麻烦。让您的IDE或Lombok为您生成它们。
Αλέκος

3

使用Java 8和Streams,我在实用程序方法中添加以下返回语句:

return results.stream().distinct().collect(Collectors.toList());

流非常快地删除重复项。我在Entity类中使用注释,如下所示:

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "STUDENT_COURSES")
private List<Course> courses;

我认为我的应用程序中还需要在需要数据库中数据的方法中使用会话。当我结束会议时。当然,将我的Entity类设置为使用轻松获取类型。我去重构。


3

我要提取2个关联的集合时遇到相同的问题:用户具有2个角色(设置)和2个进餐(列表),并且进餐重复。

@Table(name = "users")
public class User extends AbstractNamedEntity {

   @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
   @Column(name = "role")
   @ElementCollection(fetch = FetchType.EAGER)
   @BatchSize(size = 200)
   private Set<Role> roles;

   @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
   @OrderBy("dateTime DESC")
   protected List<Meal> meals;
   ...
}

DISTINCT没有帮助(DATA-JPA查询):

@EntityGraph(attributePaths={"meals", "roles"})
@QueryHints({@QueryHint(name= org.hibernate.jpa.QueryHints.HINT_PASS_DISTINCT_THROUGH, value = "false")}) // remove unnecessary distinct from select
@Query("SELECT DISTINCT u FROM User u WHERE u.id=?1")
User getWithMeals(int id);

最后,我找到了2个解决方案:

  1. 将列表更改为LinkedHashSet
  2. 仅将EntityGraph与字段“ meal”一起使用,并键入LOAD,以按声明的方式加载角色(EAGER,按BatchSize = 200以避免N + 1问题):

最终解决方案:

@EntityGraph(attributePaths = {"meals"}, type = EntityGraph.EntityGraphType.LOAD)
@Query("SELECT u FROM User u WHERE u.id=?1")
User getWithMeals(int id);

1

而不是像这样使用黑客:

  • Set 代替 List
  • criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

不会修改您的sql查询,我们可以使用(引用JPA规范)

q.select(emp).distinct(true);

确实会修改生成的sql查询,因此里面有一个DISTINCT


0

应用外部联接并带来重复的结果听起来并不好。剩下的唯一解决方案是使用流过滤结果。感谢java8提供了更简单的过滤方法。

return results.stream().distinct().collect(Collectors.toList());
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.