Автор оригинала: Vlad Mihalcea.
В этой статье мы рассмотрим, как ведет себя ключевое слово JPQL DISTINCT в зависимости от базового типа запроса сущности.
Давайте предположим, что мы используем следующие Сообщение
и Комментарий к сообщению
сущности в нашем приложении:
Объект Post
отображается следующим образом:
@Entity(name = "Post") @Table(name = "post") public class Post { @Id @GeneratedValue private Long id; private String title; @Column(name = "created_on") private LocalDate createdOn; @OneToMany( mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true ) private Listcomments = new ArrayList<>(); //Getters and setters omitted for brevity public void addComment(PostComment comment) { comments.add(comment); comment.setPost(this); } }
Метод AddComment
полезен для синхронизации обоих концов двунаправленной @OneToMany
ассоциации. Для получения более подробной информации ознакомьтесь с этой статьей .
И Комментарий к сообщению
сущность выглядит следующим образом:
@Entity(name = "PostComment") @Table(name = "post_comment") public class PostComment { @Id @GeneratedValue private Long id; @ManyToOne(fetch = FetchType.LAZY) private Post post; private String review; public PostComment() {} public PostComment(String review) { this.review = review; } //Getters and setters omitted for brevity }
По умолчанию все @ManyToOne
и @OneToOne
ассоциации должны быть извлечены с нетерпением, что чаще всего ужасная идея . Вот почему мы использовали тип FetchType.ЛЕНИВЫЙ
стратегия для @ManyToOne
ассоциации.
Данные испытаний
Теперь давайте добавим некоторые тестовые данные, которые мы собираемся использовать, чтобы продемонстрировать, как работает ключевое слово DISTINCT на основе базового типа запроса JPQL:
Post post1 = new Post(); post1.setTitle( "High-Performance Java Persistence eBook has been released!" ); post1.setCreatedOn( LocalDate.of(2016, 8, 30) ); entityManager.persist(post1); post1.addComment(new PostComment("Excellent!")); post1.addComment(new PostComment("Great!")); Post post2 = new Post(); post2.setTitle( "High-Performance Java Persistence paperback has been released!" ); post2.setCreatedOn( LocalDate.of(2016, 10, 12) ); entityManager.persist(post2); Post post3 = new Post(); post3.setTitle( "High-Performance Java Persistence Mach 1 video course has been released!" ); post3.setCreatedOn( LocalDate.of(2018, 1, 30) ); entityManager.persist(post3); Post post4 = new Post(); post4.setTitle( "High-Performance Java Persistence Mach 2 video course has been released!" ); post4.setCreatedOn( LocalDate.of(2018, 5, 8) ); entityManager.persist(post4);
При использовании скалярной проекции, подобной приведенной в следующем примере:
ListpublicationYears = 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, и Hibernate выдаст следующий результат:
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]
Поэтому для скалярных запросов ключевое слово DISTINCT
JPQL необходимо передать в базовый SQL-запрос, поскольку мы хотим, чтобы результирующий набор удалял дубликаты.
Ключевое слово DISTINCT
имеет другое назначение, когда речь заходит о запросах сущностей. Без использования DISTINCT
в спецификации JPA указано , что возвращаемые сущности, возникающие в результате СОЕДИНЕНИЯ “родитель-потомок”, могут содержать дубликаты ссылок на объекты.
Чтобы визуализировать это поведение, рассмотрим следующий JPQL-запрос:
Listposts = 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()) );
При выполнении приведенного выше запроса JPQL Hibernate генерирует следующие выходные данные:
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]
Как показано в сообщении журнала, возвращенный сообщения
Список
содержит две ссылки на один и тот же Сообщение
объект сущности. Это связано с тем, что СОЕДИНЕНИЕ дублирует родительскую запись для каждой дочерней строки, которая будет извлечена.
Чтобы удалить дубликаты ссылок на сущности, нам нужно использовать ключевое слово DISTINCT
JPQL:
Listposts = 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()) );
При выполнении приведенного выше запроса JPQL Hibernate теперь будет генерировать следующий вывод:
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]
Таким образом, дубликаты были удалены из сообщений
Списка
, но ключевое слово DISTINCT
также было передано в базовую инструкцию SQL. Для этого SQL-запроса ключевое слово DISTINCT
не служит никакой цели, так как результирующий набор будет содержать уникальные родительско-дочерние записи.
Если мы проанализируем план выполнения для предыдущей инструкции SQL, мы увидим, что в план добавляется быстрая сортировка
выполнение:
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
Выполнение quicksort
добавляет ненужные накладные расходы к выполнению нашего оператора, поскольку нам не нужно устранять какие-либо дубликаты, поскольку результирующий набор содержит уникальные комбинации строк “родитель-потомок”.
Используя режим гибернации.запрос.передайте отчетливо через подсказку запроса JPQL
Чтобы избежать передачи ключевого слова DISTINCT
в базовую инструкцию SQL, нам необходимо активировать hibernate.query.passDistinctThrough
Подсказка по запросу JPQL, как показано в следующем примере:
Listposts = 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("hibernate.query.passDistinctThrough", false) .getResultList(); LOGGER.info( "Fetched the following Post entity identifiers: {}", posts.stream().map(Post::getId).collect(Collectors.toList()) );
При запуске JPQL с помощью hibernate.запрос.передайте отчетливо
подсказка активирована, Hibernate выполняет следующий SQL-запрос:
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]
Таким образом, ключевое слово DISTINCT
больше не передается в SQL-запрос, но дубликаты сущностей удаляются из возвращаемого сообщения
Списка
.
Если мы проанализируем план выполнения для последнего SQL-запроса, мы увидим, что быстрая сортировка
выполнение больше не добавляется в план выполнения:
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
Поскольку ключевое слово DISTINCT
JPQL имеет два значения в зависимости от базового типа запроса, важно передать его в инструкцию SQL только для скалярных запросов, для которых результирующий набор требует удаления дубликатов с помощью компонента database engine.
Для запросов родительско-дочерних сущностей , в которых дочерняя коллекция использует JOIN FETCH
, ключевое слово DISTINCT
должно применяться только после получения набора результатов
из JDBC, поэтому следует избегать передачи DISTINCT
в выполняемую инструкцию SQL.