Рубрики
Без рубрики

Лучший способ использовать ключевое слово JPQL DISTINCT с JPA и Hibernate

Узнайте, как лучше всего использовать ключевое слово JPQL DISTINCT с помощью JPA и Hibernate. Для запросов сущностей используйте режим гибернации.запрос.передайте Отчетливую подсказку по запросу.

Автор оригинала: 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 List comments = 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);

При использовании скалярной проекции, подобной приведенной в следующем примере:

List 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, и 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-запрос:

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

При выполнении приведенного выше запроса 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:

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

При выполнении приведенного выше запроса 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, как показано в следующем примере:

List 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("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.