web-dev-qa-db-fra.com

Comment fonctionne DISTINCT lorsque vous utilisez JPA et Hibernate

Avec quelle colonne DISTINCT fonctionne-t-il dans JPA et est-il possible de le changer?

Voici un exemple de requête JPA utilisant DISTINCT:

select DISTINCT c from Customer c

Ce qui n'a pas beaucoup de sens - sur quelle colonne le distinct est-il basé? Est-il spécifié sur l'entité comme une annotation car je n'en ai pas trouvé?

Je voudrais spécifier la colonne sur laquelle faire la distinction, quelque chose comme:

select DISTINCT(c.name) c from Customer c

J'utilise MySQL et Hibernate.

46
Steve Claridge

Mise à jour: Voir la réponse la plus votée s'il vous plaît.

Le mien est actuellement obsolète. Seulement conservé ici pour des raisons historiques.


Distinct dans HQL est généralement nécessaire dans les jointures et non dans des exemples simples comme le vôtre.

Voir aussi Comment créer une requête distincte dans HQL

10
kazanaki

Tu es proche.

select DISTINCT(c.name) from Customer c
56
Αλέκος
@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
Tomasz

Je suis d'accord avec la réponse de kazanaki, et cela m'a aidé. Je voulais sélectionner l'entité entière, j'ai donc utilisé

 select DISTINCT(c) from Customer c

Dans mon cas, j'ai une relation plusieurs-à-plusieurs et je souhaite charger des entités avec des collections dans une seule requête.

J'ai utilisé LEFT JOIN FETCH et à la fin j'ai dû rendre le résultat distinct.

10
Yan Khonski

Comme je l'ai expliqué dans cet article , en fonction du type de requête sous-jacent JPQL ou Criteria API, DISTINCT a deux significations dans JPA.

Requêtes scalaires

Pour les requêtes scalaires, qui renvoient une projection scalaire, comme la requête suivante:

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

Le mot clé DISTINCT doit être transmis à l'instruction SQL sous-jacente car nous voulons que le moteur de base de données filtre les doublons avant de renvoyer le jeu de résultats:

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]

Requêtes d'entité

Pour les requêtes d'entité, DISTINCT a une signification différente.

Sans utiliser DISTINCT, une requête comme la suivante:

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

LOGGER.info(
    "Fetched the following Post entity identifiers: {}", 
    posts.stream().map(Post::getId).collect(Collectors.toList())
);

va JOINDRE les tables post et post_comment comme ceci:

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]

Mais les enregistrements parent post sont dupliqués dans le jeu de résultats pour chaque ligne post_comment Associée. Pour cette raison, les entités List des entités Post contiendront des références d'entité Post en double.

Pour éliminer les références d'entité Post, nous devons utiliser 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())
);

Mais alors DISTINCT est également passé à la requête SQL, et ce n'est pas du tout souhaitable:

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]

En passant DISTINCT à la requête SQL, le PLAN D'EXÉCUTION va exécuter une phase supplémentaire Sort qui ajoute une surcharge sans apporter de valeur car les combinaisons parent-enfant renvoient toujours des enregistrements uniques en raison de la colonne PK enfant:

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

Requêtes d'entité avec HINT_PASS_DISTINCT_THROUGH

Pour éliminer la phase de tri du plan d'exécution, nous devons utiliser l'indice de requête JPA HINT_PASS_DISTINCT_THROUGH:

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

Et maintenant, la requête SQL ne contiendra pas DISTINCT mais Post les doublons de référence d'entité vont être supprimés:

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]

Et le plan d'exécution va confirmer que nous n'avons plus de phase de tri supplémentaire cette fois:

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
7
Vlad Mihalcea

J'utiliserais la fonction d'expression constructeur de JPA. Voir également la réponse suivante:

Expression du constructeur JPQL - org.hibernate.hql.ast.QuerySyntaxException: la table n'est pas mappée

Suivant l'exemple de la question, ce serait quelque chose comme ça.

SELECT DISTINCT new com.mypackage.MyNameType(c.name) from Customer c
2
finrod