LINUX.ORG.RU

Эффективные запросы на JPQL

 , ,


1

3

Все пытаюсь научиться эффективно работать с JPA, да выходит не очень =)

У меня есть такая вот модель:

@Entity
public class Organization extends AbstractIdentifiable implements Serializable {
    @Column(nullable = false)
    private String title;

    @ManyToMany(mappedBy = "organizations")
    private Set<Member> members = new HashSet<>();

    @ManyToMany(mappedBy = "customerOrganization")
    private Set<Video> videos = new HashSet<>();

    @ManyToOne
    @JoinColumn
    private Playlist playlist;
}

@Entity
public class Video extends AbstractIdentifiable implements Serializable {
    @ManyToOne
    private Organization customerOrganization;

    @OneToMany(mappedBy = "video")
    private Set<ShownVideo> shownVideos = new HashSet<>();
}

@Entity
public class ShownVideo extends AbstractIdentifiable implements Serializable {
    @ManyToOne
    @JoinColumn(nullable = false)
    private Video video;
}

Весь не относящийся к делу код выпилен. Я хочу получить по каждой организации число просмотров видео, чтобы в результирующий список попали только те организации, в которых есть заданный member.

Данную задачу решаю с помощью spring-data:

public interface OrganizationRepository extends JpaRepository<Organization, Long> {
    @Query("SELECT new OrganizationCount(o, COUNT(sv)) " +
           "FROM Organization o JOIN o.members m " +
           "LEFT JOIN o.videos v " +
           "LEFT JOIN v.shownVideos sv " +
           "WHERE m = :member " +
           "GROUP BY o " +
           "ORDER BY o.title")
    public List<OrganizationCount> organizationVideoShowCountsByMember(@Param(value = "member") Member member);

}

public class OrganizationCount {
    private Organization organization;
    private long count;

    public OrganizationCount(Organization organization, long count) {
        this.organization = organization;
        this.count = count;
    }

    public Organization getOrganization() {
        return organization;
    }

    public long getCount() {
        return count;
    }
}

Данная задача решена, но очень неэффективно. Сначала выполняется корректный запрос на получение статистики:

Hibernate: select organizati0_.id as col_0_0_, count(shownvideo3_.id) as col_1_0_ from Organization organizati0_ left outer join Video videos1_ on organizati0_.id=videos1_.customerOrganization_id left outer join Video video2_ on videos1_.id=video2_.id left outer join shown_video shownvideo3_ on video2_.id=shownvideo3_.video_id group by organizati0_.id order by organizati0_.title
А затем на каждую полученную строку выполняется запрос, на получение организации:
Hibernate: select organizati0_.id as id1_3_0_, organizati0_.playlist_id as playlist3_3_0_, organizati0_.title as title2_3_0_, playlist1_.id as id1_5_1_, playlist1_.title as title2_5_1_ from Organization organizati0_ left outer join Playlist playlist1_ on organizati0_.playlist_id=playlist1_.id where organizati0_.id=?
Hibernate: select organizati0_.id as id1_3_0_, organizati0_.playlist_id as playlist3_3_0_, organizati0_.title as title2_3_0_, playlist1_.id as id1_5_1_, playlist1_.title as title2_5_1_ from Organization organizati0_ left outer join Playlist playlist1_ on organizati0_.playlist_id=playlist1_.id where organizati0_.id=?
В примере у меня их всего два. Но если будет 10 организаций будет 10 запросов. Это неимоверно тупо, учитывая то, что в первом запросе есть все колонки для формирования объекта организации. Подскажите как забороть эту ересь?

Дополнено: Если вместо

SELECT new com.helan.adkiosk.webserver.controllers.data.aggreate.OrganizationCount(o, COUNT(sv))"
писать так
SELECT o, COUNT(sv)
тогда число запросов меняется:
Hibernate: select organizati0_.id as col_0_0_, count(shownvideo3_.id) as col_1_0_, organizati0_.id as id1_3_, organizati0_.playlist_id as playlist3_3_, organizati0_.title as title2_3_ from Organization organizati0_ left outer join Video videos1_ on organizati0_.id=videos1_.customerOrganization_id left outer join Video video2_ on videos1_.id=video2_.id left outer join shown_video shownvideo3_ on video2_.id=shownvideo3_.video_id group by organizati0_.id order by organizati0_.title
Hibernate: select playlist0_.id as id1_5_0_, playlist0_.title as title2_5_0_ from Playlist playlist0_ where playlist0_.id=?

То есть первым запросом выбираются сразу и организации и статистика по ним. Только обращаться к результирующим данным приходится через список списков, что неудобно, ибо дальше этот объект передается в шаблонизатор.

Но и тут косяк, ибо потом для организаций, для которых имеются плейлисты производится выборка этих плейлистов. Они мне в принципе не нужны в том месте и их можно было бы не выбирать вовсе. В сущности Organization

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn
    private Playlist playlist;
меняла FetchType.LAZY на FetchType.EAGER - ничего не изменилось.



Последнее исправление: totik (всего исправлений: 3)

У тебя в результирующем запросе where organizati0_.id=?.

Честно скажу, не знаю точно, можно ли так поменять твой запрос, но где-то надо указать, что where organization.id in (...id_list...)

Тогда тебе выйдет список вместо одного элемента.

Аргументом функции будет список.

Deleted
()

Должно выглядеть как то так:

@Query("select s from Sample s where s.id in :ids")

Deleted
()
Последнее исправление: merhalak (всего исправлений: 1)
@Query("SELECT new OrganizationCount(o, COUNT(sv)) " +
           "FROM Organization o JOIN o.members m " +
           "LEFT JOIN o.videos v " +
           "LEFT JOIN v.shownVideos sv " +
           "WHERE m in :members " +
           "GROUP BY o " +
           "ORDER BY o.title")
public List<OrganizationCount> organizationVideoShowCountsByMember(@Param(value = "members") List<Member> members);

То есть примерно так.

Deleted
()
Ответ на: комментарий от Deleted

Вы не поняли проблему.

С другим методом, где нет фильтрации по member та же проблема:

    @Query("SELECT new OrganizationCount(o, COUNT(sv)) " +
           "FROM Organization o " +
           "LEFT JOIN o.videos v " +
           "LEFT JOIN v.shownVideos sv " +
           "GROUP BY o " +
           "ORDER BY o.title")
    public List<OrganizationCount> organizationVideoShowCounts();
Hibernate: select organizati0_.id as col_0_0_, count(shownvideo3_.id) as col_1_0_ from Organization organizati0_ left outer join Video videos1_ on organizati0_.id=videos1_.customerOrganization_id left outer join Video video2_ on videos1_.id=video2_.id left outer join shown_video shownvideo3_ on video2_.id=shownvideo3_.video_id group by organizati0_.id order by organizati0_.title
Hibernate: select organizati0_.id as id1_3_0_, organizati0_.playlist_id as playlist3_3_0_, organizati0_.title as title2_3_0_, playlist1_.id as id1_5_1_, playlist1_.title as title2_5_1_ from Organization organizati0_ left outer join Playlist playlist1_ on organizati0_.playlist_id=playlist1_.id where organizati0_.id=?
Hibernate: select organizati0_.id as id1_3_0_, organizati0_.playlist_id as playlist3_3_0_, organizati0_.title as title2_3_0_, playlist1_.id as id1_5_1_, playlist1_.title as title2_5_1_ from Organization organizati0_ left outer join Playlist playlist1_ on organizati0_.playlist_id=playlist1_.id where organizati0_.id=?

По первому запросу извлекаются все данные для того, чтобы сконструировать и объекты сущности Organization и число shownVideo по каждому из них.

Но spring data несмотря на это по каждой записи все равно извлекает Organization отдельным запросом. Классическая проблема n+1. Но причины мне непонятны.

totik
() автор топика
Последнее исправление: totik (всего исправлений: 1)
Ответ на: комментарий от paganmind

Как применить EntityGraph к JPQL-запросу с GROUP BY?

totik
() автор топика
Последнее исправление: totik (всего исправлений: 1)
Ответ на: комментарий от totik

Ты передаешь ссылку на организацию в OragnizationCount, это значит объект должен быть вытащен из базы, со всеми полями. Наверное ты ожидала, что object-relaction mapper очень умный, часть колонок замепит на объект Organization и еще одну с count отдельно вытащит. Это бы была какая-то очень хитрая фича, не уверен что такое можно сделать.

Но можно обойтись двумя запросами, первый это raw sql, на organization.id и count, а уже потом обычный hibernate query на организации, в обоих запросах использовать одно условие. Я бы так сделал.

Aber ★★★★★
()
Ответ на: комментарий от Aber

Ты передаешь ссылку на организацию в OragnizationCount, это значит объект должен быть вытащен из базы, со всеми полями. Наверное ты ожидала, что object-relaction mapper очень умный, часть колонок замепит на объект Organization и еще одну с count отдельно вытащит. Это бы была какая-то очень хитрая фича, не уверен что такое можно сделать.

Именно этого я и ожидала =) Это очевидное поведение, учитывая то, что все данные для формирования объектов выбираются из БД полностью.

Но можно обойтись двумя запросами, первый это raw sql, на organization.id и count, а уже потом обычный hibernate query на организации, в обоих запросах использовать одно условие. Я бы так сделал.

Этот вариант тоже пришел в мою голову. Как я понимаю в этом случае мне придется один раз пройтись по результирующему списку, чтобы сформировать Map с числом просмотров по организациям? Или же передавать в шаблонизатор два списка и организовывать цикл for и обращаться к обоим спискам в каждой итерации по одному списку? Есть ли третий путь?

Можно ли внедрить свой маппер и помочь spring data jpa сконструировать необходимые объекты сущностей?

totik
() автор топика
Ответ на: комментарий от totik

Мне понятнее карта.

Но вот из документации:

 Queries can return multiple objects and/or properties as an array of type Object[]:

select mother, offspr, mate.name
from DomesticCat as mother
    inner join mother.mate as mate
    left outer join mother.kittens as offspr

Возможно сработало бы в твоем запросе такое:

    @Query("SELECT o, new OrganizationCount(o, COUNT(sv)) " +
           "FROM Organization o " +
           "LEFT JOIN o.videos v " +
           "LEFT JOIN v.shownVideos sv " +
           "GROUP BY o " +
           "ORDER BY o.title")
В самом начале добавилось SELECT o,... но этот запрос вернет Object[] из которого Object[1] будет OrganizationCount. Испытый, поменяй возвращаемый тип на Object[], посмотри что будет с выборкой.

Aber ★★★★★
()
Ответ на: комментарий от Aber
org.hibernate.hql.internal.ast.QuerySyntaxException: unexpected token: o near line 1, column 8

Но зато работает так:

    @Query("SELECT o, COUNT(sv) " +
           "FROM Organization o " +
           "LEFT JOIN o.videos v " +
           "LEFT JOIN v.shownVideos sv " +
           "GROUP BY o " +
           "ORDER BY o.title")
Получается List<List<?>>. Ладно, придется делать два запроса: первым получать список организаций с нужным EntityGraph, а вторым запросом получать число просмотров.

Жаль, что JPQL и Spring Data JPA не дают той гибкости, которой можно добиться успользуя нативный SQL и ручной маппинг данных.

totik
() автор топика
Ответ на: комментарий от totik

Жаль, что JPQL и Spring Data JPA не дают той гибкости, которой можно добиться успользуя нативный SQL и ручной маппинг данных.

Как бы да, и если совсем философски подходить к вопросу, то так везде, и за пределами sql, и программирования в частности. Абстракция скрывает сложность, но накладывает ограничения.

Есть еще hibernate criteria queries api который позволяет составлять запросы программным способом, обычно используют когда запрос нужно динамически в рантайме конструировать, но не уверен что это тебе нужно.

Aber ★★★★★
()
Ответ на: комментарий от bvn13

Вы не могли бы привести пример, как в данном случае сконструировать список объектов OrganizationCount?

totik
() автор топика
Ответ на: комментарий от Aber

Первый раз увидел, похоже да, тоже самое, видно стандартизировали.

В моем случае это из пушки по воробьям. Spring data JPA тем и хорошо, что позволяем описывать выборку в интерфейсах без реализации методов. Понятные запросы, либо же понятные названия методов, по которым генерируются запросы. Красота =)

Лучше я выполню два запроса и получу, на что рассчитывала изначально. Главное, чтобы без n + 1 запросов.

totik
() автор топика
Ответ на: комментарий от totik

Получается List<List<?>>

Должен был получиться List<Object[]>, потому код мог бы быть таким:

public interface OrganizationRepository extends JpaRepository<Organization, Long> {
    @Query("SELECT o, COUNT(sv) " +
           "FROM Organization o " +
           "LEFT JOIN o.videos v " +
           "LEFT JOIN v.shownVideos sv " +
           "GROUP BY o " +
           "ORDER BY o.title")
    public List<Object[]> organizationToVideoShowCounts();

    public default List<OrganizationCount> getOrganizationCount() {
        List<OrganizationCount> result = new ArrayList<>();
        for (Object[] item: this.organizationToVideoShowCounts()) {
            result.add(new OrganizationCount(item[0], item[1]));
        }
        return result;
    }
}

Правда тогда работа коллекцией становится не lazy, хибернет ведь возвращает свою коллекцию с записями которые будут подружатся по мере итерирования, чтоб сделать lazy можно stream использовать, но это слишком сложно для программирования на лоре.

Aber ★★★★★
()
Последнее исправление: Aber (всего исправлений: 1)
Ответ на: комментарий от totik

сконструировать список объектов OrganizationCount?

Сделать отдельно репозиторий, который возвращает List<Object[]> и сервис, который через репозиторий получает List<Object[]> и конвертирует его в List<OrganisationCount>?

Tanger ★★★★★
()
Ответ на: комментарий от Tanger

Сделать отдельно репозиторий, который возвращает List<Object[]> и сервис, который через репозиторий получает List<Object[]> и конвертирует его в List<OrganisationCount>?

Тогда не обойтись без лишнего прохода по контейнеру.

totik
() автор топика
Последнее исправление: totik (всего исправлений: 1)
Ответ на: комментарий от totik

@Query(nativeQuery = true, 
           value = "SELECT o.*, COUNT(sv) as count " +
           "FROM Organization o JOIN o.members m " +
           "LEFT JOIN o.videos v " +
           "LEFT JOIN v.shownVideos sv " +
           "WHERE m = :member " +
           "GROUP BY o " +
           "ORDER BY o.title")
List<OrganizationCount> List<OrganizationCount> organizationVideoShowCountsByMember(@Param(value = "member") Member member);

Пишу навскидку. У меня при выборе t.* все поля сам замаппил в объект типа Table. Главное - чтобы сеттеры были у полей и имена совпадали с выбираемыми в запросе полями.

bvn13 ★★★★★
()
Последнее исправление: bvn13 (всего исправлений: 1)
Ответ на: комментарий от matroskin

Вообще спринг-дата умеет в custom-repository

Ага. Использовал такой.

totik: Тогда не обойтись без лишнего прохода по контейнеру.

Если это реально критично - можно cделать свой враппер над List<Object[]>.

Tanger ★★★★★
()

Выше уже предложили про EntityGraph и нативный запрос(он вернет Object[] и придется мепить его руками)

Еще в JPQL есть JOIN FETCH Попробуйте:

    @Query("SELECT new OrganizationCount(o, COUNT(sv)) " +
           "FROM Organization o JOIN o.members m " +
           "JOIN FETCH o.playlist " +
           "LEFT JOIN o.videos v " +
           "LEFT JOIN v.shownVideos sv " +
           "WHERE m = :member " +
           "GROUP BY o " +
           "ORDER BY o.title")
    public List<OrganizationCount> organizationVideoShowCountsByMember(@Param(value = "member") Member member);

mythCreator
()
Ответ на: комментарий от mythCreator

FETCH есть, но он не применим в этом случае. Завершается ошибкой аля «нельзя использовать FETCH для сущностей, которые не участвуют в выборке».

totik
() автор топика
Ответ на: комментарий от totik

Да, я не внимательно прочитал вопрос и подумал, что тут обычный N+1 из-за того, что Playlist не сразу подтягивается.

То есть первым запросом выбираются сразу и организации и статистика по ним. Только обращаться к результирующим данным приходится через список списков, что неудобно, ибо дальше этот объект передается в шаблонизатор.

List<Object[]> ? Вы же не из контроллера репозиторий вызываете? Добавьте тогда в OrganizationCount конструктор, который принимал бы Object[] и сделайте метод, который с помощью stream + map сделает List<OrganizationCount>. Согласен, что это не очень удобно, но с не стандартными запросами почти всегда так.

Но и тут косяк, ибо потом для организаций, для которых имеются плейлисты производится выборка этих плейлистов. Они мне в принципе не нужны в том месте и их можно было бы не выбирать вовсе. В сущности Organization

Скорее всего где-то вызываются геттеры для плейлистов. Если вы в шаблонизаторе сами их не вызываете и не сериализуете объекты то у меня нету версий почему это могло произойти. Попробуйте вывести в геттерах Thread.currentThread().getStackTrace() или поставить брек поинты, чтобы узнать где они вызываются.

То что с new OrganizationCount и без него генерируются разные запросы странно и с большой вероятностью баг хибернейта. Можно попробовать явно указать в GROUP BY все поля Organization и явно передать эти поля в конструктор OrganizationCount.

mythCreator
()

Если есть необходимость так «вылизывать» работу с БД то я бы рекомендовал посмотреть на MyBatis, а не на ORM-ы. С другой стороны есть ли в этом реальная необходимость? Ведь оптимизация <=> увеличение трудозатрат разработчика и, с большой вероятностью, привязка к конкретным «фишкам» именно твоей СУБД. Если это перфекционизм, а не реальная потребность проекта то оно, ИМХО, того не стоит.

anonymous
()
Ответ на: комментарий от anonymous

Если есть необходимость так «вылизывать» работу с БД то я бы рекомендовал посмотреть на MyBatis, а не на ORM-ы. С другой стороны есть ли в этом реальная необходимость? Ведь оптимизация <=> увеличение трудозатрат разработчика и, с большой вероятностью, привязка к конкретным «фишкам» именно твоей СУБД. Если это перфекционизм, а не реальная потребность проекта то оно, ИМХО, того не стоит.

Нагенерировала для своего проекта тестовых данных - по 1000 записей на каждую сущность и связь. Что-то еле ворочается, что-то вообще падает исчерпав память. И все из-за того, что при попытке запросить все связанные данные по одной сущности с помощью Named Entity Graph результирующий запрос порождает декартово произведение всех всех связей. При двух списков в одной сущности - это миллион записей. А кое-где их и три и четыре.

Так что лучше я займусь оптимизацией сейчас и буду получать данные самописными JPQL запросами в spring data, вызывая несколько методов, вместо одного с Named Entity Graph.

За MyBatis спасибо! Погляжу на него после этого проекта.

totik
() автор топика
Последнее исправление: totik (всего исправлений: 2)
Ответ на: комментарий от mythCreator

Скорее всего где-то вызываются геттеры для плейлистов. Если вы в шаблонизаторе сами их не вызываете и не сериализуете объекты то у меня нету версий почему это могло произойти. Попробуйте вывести в геттерах Thread.currentThread().getStackTrace() или поставить брек поинты, чтобы узнать где они вызываются.

Все оказалось гораздо проще:

public @interface OneToMany {
    FetchType fetch() default LAZY;
}

public @interface ManyToOne {
    FetchType fetch() default EAGER;
}

У меня везде использовался тип загрузки по умолчанию. Прописала явно как LAZY и все стало хорошо.

totik
() автор топика
Последнее исправление: totik (всего исправлений: 2)
Ответ на: комментарий от totik

Нагенерировала для своего проекта тестовых данных - по 1000 записей на каждую сущность и связь. Что-то еле ворочается, что-то вообще падает исчерпав память. И все из-за того, что при попытке запросить все связанные данные по одной сущности с помощью Named Entity Graph результирующий запрос порождает декартово произведение всех всех связей. При двух списков в одной сущности - это миллион записей. А кое-где их и три и четыре.

В хибернейте нельзя иметь несколько списков в одной сущности как раз потому, что это порождает декартово произведение(cartesian product), а поскольку список может хранить повторяющиеся элементы сделать однозначную проекцию по картезиану не получается.

Например: {1,1} x {2} = {1} x {2,2} = {(1;2), (1;2)}

Поэтому хибернейт кидает в таких случаях MultipleBagFetchException. Вместо List надо использовать Set.

mythCreator
()
Ответ на: комментарий от mythCreator

Да, я оговорилась, не только списки, но и множества.

Использование Set не избавляет от декартового произведения в результирующей таблице sql-запроса. Все равно нужно будет выкачать те миллиарды записей, что подготовит СУБД и потом уже провайдер JPA все разложит по сетам.

Так что я отказалась от вытягивания связей OneToMany и ManyToMany больше одной за раз.

Если вручную формировать сущности на кастомных запросах все гораздо эффективнее можно написать, чем то, что предлагает. JPA/Hibernate.

totik
() автор топика
Последнее исправление: totik (всего исправлений: 3)

Если ты нуб и тоже озабочен эффективностью работы с СУБД с помощью hibernate, то включай статистику: https://www.thoughts-on-java.org/how-to-activate-hibernate-statistics-to-anal...

Мне очень помогло для выявления узких мест. Сразу видно не только запросы, но и число выбранным строк по ним.

totik
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.