Le problème que vous rencontrez est lié à la façon dont vous utilisez le HINT_PASS_DISTINCT_THROUGH
indice.
Cet indice vous permet d'indiquer à Hibernate que le DISTINCT
le mot-clé ne doit pas être utilisé dans le SELECT
déclaration émise contre la base de données.
Vous profitez de ce fait pour permettre à vos requêtes d'être triées par un champ qui n'est pas inclus dans le DISTINCT
liste des colonnes.
Mais ce n'est pas ainsi que cet indice doit être utilisé.
Cet indice ne doit être utilisé que lorsque vous êtes sûr qu'il n'y aura pas de différence entre appliquer ou non un DISTINCT
mot-clé au SQL SELECT
déclaration, car le SELECT
l'instruction va déjà récupérer toutes les valeurs distinctes en soi . L'idée est d'améliorer les performances de la requête en évitant l'utilisation d'un DISTINCT
inutile déclaration.
C'est généralement ce qui se passe lorsque vous utilisez le query.distinct
méthode dans vos requêtes de critères, et vous êtes en train de join fetching
relations avec les enfants. Ce super article
de @VladMihalcea expliquent en détail le fonctionnement de l'indice.
D'autre part, lorsque vous utilisez la pagination, il définira OFFSET
et LIMIT
- ou quelque chose de similaire, selon la base de données sous-jacente - dans le SQL SELECT
déclaration émise contre la base de données, limitant à un nombre maximum de résultats votre requête.
Comme indiqué, si vous utilisez le HINT_PASS_DISTINCT_THROUGH
indice, le SELECT
l'instruction ne contiendra pas le DISTINCT
mot-clé et, à cause de vos jointures, cela pourrait potentiellement donner des enregistrements en double de votre entité principale. Ces enregistrements seront traités par Hibernate pour différencier les doublons, car vous utilisez query.distinct
, et il supprimera en fait les doublons si nécessaire. Je pense que c'est la raison pour laquelle vous pouvez obtenir moins d'enregistrements que demandé dans votre Pageable
.
Si vous supprimez l'indice, comme le DISTINCT
mot-clé est passé dans l'instruction SQL qui est envoyée à la base de données, dans la mesure où vous ne projetez que les informations de l'entité principale, il récupérera tous les enregistrements indiqués par LIMIT
et c'est pourquoi il vous donnera toujours le nombre d'enregistrements demandé.
Vous pouvez essayer de fetch join
vos entités enfants (au lieu de seulement join
avec eux). Cela éliminera le problème de ne pas pouvoir utiliser le champ dont vous avez besoin pour trier dans les colonnes du DISTINCT
mot-clé et, en plus, vous pourrez appliquer, désormais légitimement, l'indice.
Mais si vous le faites, cela vous posera un autre problème :si vous utilisez le join fetch et la pagination, pour renvoyer les entités principales et ses collections, Hibernate n'appliquera plus la pagination au niveau de la base de données - il n'inclura pas OFFSET
ou LIMIT
mots-clés dans l'instruction SQL, et il essaiera de paginer les résultats en mémoire. C'est le fameux Hibernate HHH000104
avertissement :
HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
@VladMihalcea explique cela en détail dans la dernière partie de ceci article.
Il a également proposé une solution possible à votre problème, Fonctions de fenêtre .
Dans votre cas d'utilisation, au lieu d'utiliser Specification
s, l'idée est que vous implémentiez votre propre DAO. Ce DAO doit uniquement avoir accès au EntityManager
, ce qui n'est pas beaucoup car vous pouvez injecter votre @PersistenceContext
:
@PersistenceContext
protected EntityManager em;
Une fois que vous avez ce EntityManager
, vous pouvez créer des requêtes natives et utiliser des fonctions de fenêtre pour créer, en fonction du Pageable
fourni informations, la bonne instruction SQL qui sera émise sur la base de données. Cela vous donnera beaucoup plus de liberté sur les champs à utiliser pour le tri ou tout ce dont vous avez besoin.
Comme l'indique le dernier article cité, Window Functions est une fonctionnalité prise en charge par toutes les bases de données principales.
Dans le cas de PostgreSQL, vous pouvez facilement les trouver dans la documentation officielle .
Enfin, une autre option, suggérée en fait par @nickshoe, et expliquée en détail dans le article il a cité, consiste à effectuer le processus de tri et de pagination en deux phases :dans la première phase, vous devez créer une requête qui référencera vos entités enfants et dans laquelle vous appliquerez la pagination et le tri. Cette requête vous permettra d'identifier les identifiants des entités principales qui seront utilisées, dans la deuxième phase du processus, pour obtenir les entités principales elles-mêmes.
Vous pouvez tirer parti du DAO personnalisé susmentionné pour accomplir ce processus.