使用JPA和Hibernate时,DISTINCT是如何工作的?

73

DISTINCT在JPA中使用时是和哪个列配合使用的?是否可以更改它所配合的列?

这里有一个使用DISTINCT的JPA查询示例:

select DISTINCT c from Customer c

这并不太合理 - 根据哪一列进行去重?它是否在实体上作为注释指定,因为我找不到呢?

我想指定用于去重的列,类似于:

select DISTINCT(c.name) c from Customer c

我正在使用MySQL和Hibernate。


@Id在实体的生命周期中扮演什么具体角色? - Samuel Owino
7个回答

77

你离成功很近了。

select DISTINCT(c.name) from Customer c

33
这只会返回该列的一个数组。如何使用这种方法返回整个实体? - cen
5
你所要求的是不合逻辑的。如果我有两个客户 (id=1234, name="Joe Customer") 和 (id=2345, name="Joe Customer"),在这样的查询中应该返回哪一个?结果将是未定义的。现在,你可以通过类似以下语法来强制进行选择(我不确定具体的语法是什么,但这应该能够给出一般的想法): select c from Customer c where id in (select min(d.id) from Customer d group by d.name) ... 但这是一个情况相关的问题,因为你需要根据可用的属性来选择一个实体。 - Jules
@Jules - 在这种情况下,通常您并不真正关心返回哪一个,因此任何选择技术都可以。我认为mysql甚至默认处理这种情况。不过我不记得我两年前的确切用例了。 - cen
@Jules,有没有一种方法可以将返回的对象数组与实体进行映射? - greenhorn

37

根据基础的JPQL或Criteria API查询类型,DISTINCT在JPA中有两个含义。

标量查询

对于返回标量投影的标量查询,比如下面的查询:

List<Integer> publicationYears = entityManager.createQuery("""
    select distinct year(p.createdOn)
    from Post p
    order by year(p.createdOn)
    """, Integer.class)
.getResultList();

LOGGER.info("Publication years: {}", publicationYears);

我们需要将DISTINCT关键字传递给底层的SQL语句,因为我们希望DB引擎在返回结果集之前过滤重复项:

SELECT DISTINCT
    extract(YEAR FROM p.created_on) AS col_0_0_
FROM
    post p
ORDER BY
    extract(YEAR FROM p.created_on)

-- Publication years: [2016, 2018]

Hibernate 6

Hibernate 6可以自动消除父实体引用的重复,因此您无需像在Hibernate 5中那样使用DISTINCT关键字。

因此,在运行以下查询时:

List<Post> posts = entityManager.createQuery("""
    select p
    from Post p
    left join fetch p.comments
    where p.title = :title
    """, Post.class)
.setParameter(
    "title", 
    "High-Performance Java Persistence eBook has been released!"
)
.getResultList();

assertEquals(1, posts.size());
assertEquals(2, posts.get(0).getComments().size());

即使它有两个关联的PostComment子实体,我们可以看到仅获取了单个Post实体。

Hibernate 5实体查询

在JPA中,对于实体查询,DISTINCT有不同的含义。

如果不使用DISTINCT,像下面这样的查询:

List<Post> posts = entityManager.createQuery("""
    select distinct p
    from Post p
    left join fetch p.comments
    where p.title = :title
    """, Post.class)
.setParameter(
    "title", 
    "High-Performance Java Persistence eBook has been released!"
)
.getResultList();

LOGGER.info(
    "Fetched the following Post entity identifiers: {}", 
    posts.stream().map(Post::getId).collect(Collectors.toList())
);
is going to JOIN the post and the post_comment tables like this:
SELECT p.id AS id1_0_0_,
       pc.id AS id1_1_1_,
       p.created_on AS created_2_0_0_,
       p.title AS title3_0_0_,
       pc.post_id AS post_id3_1_1_,
       pc.review AS review2_1_1_,
       pc.post_id AS post_id3_1_0__
FROM   post p
LEFT OUTER JOIN
       post_comment pc ON p.id=pc.post_id
WHERE
       p.title='High-Performance Java Persistence eBook has been released!'

-- Fetched the following Post entity identifiers: [1, 1]

但是每个相关联的post_comment行在结果集中都会复制父post记录。因此,Post实体的List将包含重复的Post实体引用。

要消除Post实体引用,我们需要使用DISTINCT

List<Post> posts = entityManager.createQuery("""
    select distinct p
    from Post p
    left join fetch p.comments
    where p.title = :title
    """, Post.class)
.setParameter(
    "title", 
    "High-Performance Java Persistence eBook has been released!"
)
.getResultList();
 
LOGGER.info(
    "Fetched the following Post entity identifiers: {}", 
    posts.stream().map(Post::getId).collect(Collectors.toList())
);

但是SQL查询语句中也传递了DISTINCT,这一点并不理想:

SELECT DISTINCT
       p.id AS id1_0_0_,
       pc.id AS id1_1_1_,
       p.created_on AS created_2_0_0_,
       p.title AS title3_0_0_,
       pc.post_id AS post_id3_1_1_,
       pc.review AS review2_1_1_,
       pc.post_id AS post_id3_1_0__
FROM   post p
LEFT OUTER JOIN
       post_comment pc ON p.id=pc.post_id
WHERE
       p.title='High-Performance Java Persistence eBook has been released!'
 
-- Fetched the following Post entity identifiers: [1]

通过向SQL查询传递DISTINCT,执行计划将执行额外的排序阶段,这会增加开销,但不会带来任何价值,因为由于子PK列,父子组合始终返回唯一记录:

Unique  (cost=23.71..23.72 rows=1 width=1068) (actual time=0.131..0.132 rows=2 loops=1)
  ->  Sort  (cost=23.71..23.71 rows=1 width=1068) (actual time=0.131..0.131 rows=2 loops=1)
        Sort Key: p.id, pc.id, p.created_on, pc.post_id, pc.review
        Sort Method: quicksort  Memory: 25kB
        ->  Hash Right Join  (cost=11.76..23.70 rows=1 width=1068) (actual time=0.054..0.058 rows=2 loops=1)
              Hash Cond: (pc.post_id = p.id)
              ->  Seq Scan on post_comment pc  (cost=0.00..11.40 rows=140 width=532) (actual time=0.010..0.010 rows=2 loops=1)
              ->  Hash  (cost=11.75..11.75 rows=1 width=528) (actual time=0.027..0.027 rows=1 loops=1)
                    Buckets: 1024  Batches: 1  Memory Usage: 9kB
                    ->  Seq Scan on post p  (cost=0.00..11.75 rows=1 width=528) (actual time=0.017..0.018 rows=1 loops=1)
                          Filter: ((title)::text = 'High-Performance Java Persistence eBook has been released!'::text)
                          Rows Removed by Filter: 3
Planning time: 0.227 ms
Execution time: 0.179 ms

Hibernate 5带有HINT_PASS_DISTINCT_THROUGH的实体查询

为了从执行计划中消除排序阶段,我们需要使用HINT_PASS_DISTINCT_THROUGH JPA查询提示:

List<Post> posts = entityManager.createQuery("""
    select distinct p
    from Post p
    left join fetch p.comments
    where p.title = :title
    """, Post.class)
.setParameter(
    "title", 
    "High-Performance Java Persistence eBook has been released!"
)
.setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false)
.getResultList();
 
LOGGER.info(
    "Fetched the following Post entity identifiers: {}", 
    posts.stream().map(Post::getId).collect(Collectors.toList())
);

现在,SQL查询将不包含DISTINCT,但是Post实体引用的重复项将被删除:

SELECT
       p.id AS id1_0_0_,
       pc.id AS id1_1_1_,
       p.created_on AS created_2_0_0_,
       p.title AS title3_0_0_,
       pc.post_id AS post_id3_1_1_,
       pc.review AS review2_1_1_,
       pc.post_id AS post_id3_1_0__
FROM   post p
LEFT OUTER JOIN
       post_comment pc ON p.id=pc.post_id
WHERE
       p.title='High-Performance Java Persistence eBook has been released!'
 
-- Fetched the following Post entity identifiers: [1]

而执行计划将确认这一次我们不再有额外的排序阶段:

Hash Right Join  (cost=11.76..23.70 rows=1 width=1068) (actual time=0.066..0.069 rows=2 loops=1)
  Hash Cond: (pc.post_id = p.id)
  ->  Seq Scan on post_comment pc  (cost=0.00..11.40 rows=140 width=532) (actual time=0.011..0.011 rows=2 loops=1)
  ->  Hash  (cost=11.75..11.75 rows=1 width=528) (actual time=0.041..0.041 rows=1 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 9kB
        ->  Seq Scan on post p  (cost=0.00..11.75 rows=1 width=528) (actual time=0.036..0.037 rows=1 loops=1)
              Filter: ((title)::text = 'High-Performance Java Persistence eBook has been released!'::text)
              Rows Removed by Filter: 3
Planning time: 1.184 ms
Execution time: 0.160 ms
如果您使用的是Hibernate 6,则不再需要QueryHints.HINT_PASS_DISTINCT_THROUGH,应将其删除,因为该功能已从框架中删除。

2
上周买的,虽然还没有看完;-) 可能是我读过的最好的IT书籍。 - Jaqen H'ghar
6
谢谢,非常有用的答案!我在阅读了你提到的文章和Spring Data JPA参考文档后,在我的Spring Data JPA Repository方法顶部添加了这个注解来实现:@QueryHints(@QueryHint(name = "hibernate.query.passDistinctThrough", value = "false")) - dk7
1
@dk7 这正是我所寻找的。谢谢! - charvi
但是计划时间增加了,为什么会这样? - muasif80
2
@İsmailYavuz PASS_DISTINCT_THROUGH 是由 HHH-10965 实现的,自 Hibernate ORM 5.2.2 起可用。Spring Boot 1.5.9 非常老,使用的是 Hibernate ORM 5.0.12。因此,如果您想从这些令人惊叹的功能中受益,您需要升级您的依赖项。 - Vlad Mihalcea
显示剩余2条评论

13
@Entity
@NamedQuery(name = "Customer.listUniqueNames", 
            query = "SELECT DISTINCT c.name FROM Customer c")
public class Customer {
        ...

        private String name;

        public static List<String> listUniqueNames() {
             return = getEntityManager().createNamedQuery(
                   "Customer.listUniqueNames", String.class)
                   .getResultList();
        }
}

13

更新:请查看得分最高的答案。

我的回答现在已经过时。仅因历史原因保留在此处。


HQL中的Distinct通常在连接操作中使用,而非像你的简单例子那样使用。

参见如何在HQL中创建Distinct查询


13
没别的意思,但这怎么可能被接受为答案呢? - Pieter De Bie
1
这是从2009年到2012年唯一有效的答案。 - kazanaki

12

我同意kazanaki的答案,它对我很有帮助。 我想选择整个实体,所以我使用了

 select DISTINCT(c) from Customer c
在我的情况下,我有一个多对多的关系,并且我希望在一次查询中加载带有集合的实体。
我使用了LEFT JOIN FETCH,最后不得不使结果变得唯一。

5

0

我正在添加一个略微具体的答案,以防某人遇到与我相同的问题并找到了这个问题。

我使用带有查询注释的JPQL(无查询构建)。

我需要获取嵌入到另一个实体中的实体的不同值,该关系通过ManyToOne注释进行断言。

我有两个数据库表:

  • MainEntity,我想得到其不同的值
  • LinkEntity,它是MainEntity和另一个表之间的关系表。 它具有由其三个列组成的复合主键。

在Java Spring代码中,这导致实现了三个类:

LinkEntity :

@Entity
@Immutable
@Table(name="link_entity")
public class LinkEntity implements Entity {

    @EmbeddedId
    private LinkEntityPK pk;

    // ... Getter, setter, toString()
}

LinkEntityPK:

@Embeddable
public class LinkEntityPK implements Entity, Serializable {

    /** The main entity we want to have distinct values of */
    @ManyToOne
    @JoinColumn(name = "code_entity")
    private MainEntity mainEntity;

    /** */
    @Column(name = "code_pk2")
    private String codeOperation;

    /** */
    @Column(name = "code_pk3")
    private String codeFonction;

主实体:

@Entity
@Immutable
@Table(name = "main_entity")
public class MainEntity implements Entity {

    /** We use this for LinkEntity*/
    @Id
    @Column(name="code_entity")
    private String codeEntity;


    private String name;
    // And other attributes, getters and setters

因此,获取主实体的不同值的最终查询为:

@Repository
public interface EntityRepository extends JpaRepository<LinkEntity, String> {

    @Query(
        "Select " +
            "Distinct linkEntity.pk.intervenant " +
        "From " +
            "LinkEntity as linkEntity " +
            "Join MainEntity as mainEntity On " +
                 "mainEntity = linkEntity.pk.mainEntity ")
    List<MainEntity> getMainEntityList();

}

希望这能帮助到某个人。

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