В этой статье вы узнаете, как расширение JPAstreamer Quarkus упрощает выполнение типо‑безопасных запросов Hibernate без излишней многословности и сложности.

Насколько выразителен конструктор JPA Criteria, настолько же многословными часто бывают JPA запросы, что сам API может быть не интуитивным в использовании, особенно для новичков. В экосистеме Quarkus Panache является частичным решением этих проблем при использовании Hibernate. 

Тем не менее я ловлю себя на том, что часто приходится жонглировать вспомогательными методами Panache, предварительно сконфигурированными  перечислениями и raw-строками при составлении любых отличных от простейших запросов. 

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

Таким образом, пользовательский опыт написания JPA запросов может быть улучшен в этом направлении. 

Введение

Одним из оставшихся недостатков является то, что raw-строки по своей природе не типо‑безопасны, а это означает, что моя IDE отказывает мне в помощи авто завершения кода и в лучшем случае желает мне удачи. 

С другой стороны, Quarkus позволяет перезапускать приложения за доли секунды, чтобы вынести быстрый вердикт моему коду. И ничто не сравнится с искренней радостью и неподдельным удивлением, когда я составляю рабочий запрос не с десятой, а с пятой попытки... 

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

Она достигает этой цели, позволяя выражать запросы могут быть выражены в виде стандартных потоков Java. После выполнения JPAstreamer переводит потоковый конвейер в HQL-запрос для эффективного выполнения и не создает никаких объектов, нерелевантных результатам запроса.

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

@Entity
@Table(name = "person")
public class Person {
  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "person_id", nullable = false, updatable = false)
    private Integer actorId;

    @Column(name = "first_name", nullable = false, columnDefinition = "varchar(45)")
    private String firstName;

    @Column(name = "last_name", nullable = false, columnDefinition = "varchar(45)")
    private String lastName;

    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

	// Getters for all fields will follow from here 
}

Чтобы получить Person с идентификатором 1 с помощью JPAstreamer, все, что вам нужно, это выполнить следующий код:

@ApplicationScoped
public class PersonRepository {
	
    @PersistenceContext
    EntityManagerFactory entityManagerFactory;

    private final JPAStreamer jpaStreamer;

    public PersonRepository EntityManagerFactory entityManagerFactory) {
		jpaStreamer = JPAStreamer.of(entityManagerFactory); <1>
    }

    @Override
    public Optional<Person> getPersonById(int id) {
        return this.jpaStreamer.from(Person.class) <2>
            .filter(Person$.personId.equal(id)) <3>
            .findAny();
    }
  
}

<1> Инициализируем JPAstreamer одной строкой, базовый JPA-провайдер обрабатывает конфигурацию БД.

<2> Источником потока является таблица Person.

<3> Операция фильтрации обрабатывается как предложение SQL WHERE, и условие выражается типо‑безопасным образом с помощью предикатов JPAstreamer (подробнее об этом будет сказано далее). 

Несмотря на то, что выглядит так, будто JPAstreamer работает со всеми объектами Person, конвейер оптимизирован для одного запроса, в данном случае:

select
    person0_.person_id as person_id1_0_,
    person0_.first_name as first_na2_0_,
    person0_.last_name as last_nam3_0_,
    person0_.created_at as created_4_0_,
from
    person person0_
where
    person0_.person_id=1

Таким образом, создается только объект Person, соответствующий критериям поиска. 

Далее мы рассмотрим более сложный пример, в котором выполняется поиск объектов Person, с именем заканчивающимся на «А», а фамилия начинается на «Б». 

Результаты поиска сортируются в первую очередь по имени, а затем по фамилии. Далее я решаю применить смещение 5, исключая первые пять результатов, и ограничить общее количество результатов до 10. Вот конвейер потока для решения этой задачи:

List<Person> list = jpaStreamer.stream(Person.class)
	.filter(Person$.firstName.endsWith("A").and(Person$.lastName.startsWith("B"))) <1>
	.sorted(Person$.firstName.comparator().thenComparing(Person$.lastName.comparator())) <2>
	.skip(5) <3> 
	.limit(10) <4>
	.collect(Collectors.toList())

<1> Фильтры можно комбинировать с операторами and/or.

<2> Простая фильтрация по одному или нескольким свойствам.

<3> Пропустить первых 5 результатов поиска.

<4> Возвращать не более 10 человек.

В контексте запросов потоковые операторы filter, sort, limit и skip имеют естественное отображение, которое делает результирующий запрос выразительным и интуитивно понятным для чтения, оставаясь при этом компактным. 

Этот запрос переводится JPAstreamer в следующий оператор HQL:

select
    person0_.person_id as person_id1_0_,
    person0_.first_name as first_na2_0_,
    person0_.last_name as last_nam3_0_,
    person0_.created_at as created_4_0_,
from
    person person0_
where
    (person0_.first_name like ?) 
    and (person0_.last_name like ?) 
order by
    person0_.first_name asc,
    person0_.last_name asc limit ?, ?

Как работает JPAstreamer

Хорошо, это выглядит просто. Но как это работает? JPAstreamer использует процессор аннотаций для формирования мета-модели во время компиляции. Он проверяет все классы, отмеченные стандартной аннотацией JPA @Entity, и для каждой сущности  Foo.class создается соответствующий класс Foo$.class. Созданные классы представляют атрибуты сущности как поля, используемые для формирования предикатов вида User$.firstName.startsWith("A"), которые могут быть интерпретированы оптимизатором запросов JPAstreamer.

Стоит повторить, что JPAstreamer не изменяет и не нарушает существующую кодовую базу, а просто расширяет API для обработки потоковых запросов Java.

Установка расширения JPAstreamer

JPAstreamer устанавливается, как и любое другое расширение Quarkus, с помощью зависимости Maven:

<dependency>
	<groupId>io.quarkiverse.jpastreamer</groupId>
	<artifactId>quarkus-jpastreamer</artifactId>
	<version>1.0.0</version>
</dependency>

После добавления зависимости перестройте приложение Quarkus, чтобы запустить процессор аннотаций JPAstreamer. Установка будет завершена, когда сгенерированные поля появятся в каталоге /target/generated-sources. Вы узнаете их по последнему символу $ в именах классов, например, Person$.class.

Примечание: JPAstreamer требует наличия базового JPA-провайдера, например, Hibernate. По этой причине JPAstreamer не нуждается в дополнительной настройке, поскольку интеграция с базой данных обеспечивается JPA-провайдером.

JPAstreamer и Panache 

Любой поклонник Panache заметит, что JPAstreamer разделяет некоторые цели с Panache, упрощая многие распространенные запросы. Тем не менее, JPAstreamer отличается тем, что вселяет больше уверенности в запросах благодаря своему типо-безопасному потоковому интерфейсу. Однако никому не приходится выбирать, поскольку Panache и JPAstreamer прекрасно работают вместе друг с другом.

Примечание: Вот пример приложения Quarkus, в котором используются и JPAstreamer, и Panache.

На момент написания статьи JPAstreamer не поддерживал шаблон Active Record от Panache, поскольку он полагается на стандартные JPA сущности для создания своей мета-модели. Вероятно, в ближайшем будущем это изменится.

Резюме

JPA в целом и Hibernate значительно упростили доступ приложений к базам данных, но их API иногда создает ненужную сложность. С помощью JPAstreamer вы можете использовать JPA, сохраняя свою кодовую базу чистой и удобной для сопровождения.

Комментарии (2)


  1. breninsul
    00.00.0000 00:00
    +1

    Что за кошмар, дырявые абстракции все дырявее и дырявее.

    "упрощая многие распространенные запрос"- просто объясните мне в чем сложность сделать обычную выборку или

    jsonb_build_object(нужные поля, агрегации в массивы, что угодно )?

    Вот какую задачу решают ОРМы?

    Помнить про ленивую инициализацию?

    Любиться с кривыми селектами в базу?

    Апдейтать все поля целиком вместо инкремента 1го столбца?

    Стимулировать использовать единый обьект в любых ситуациях, даже когда нам нужны всего пару полей?


    1. mitasov-ra
      00.00.0000 00:00

      Бугурт вышел немного агрессивный, но полностью согласен с каждым предложением.

      ОРМ в большинстве случаев кажется абстракцией ради абстракций. Перешёл на чистый jdbc у себя в проекте, чтобы избавиться от оверхеда JPA и Hibernate, и оказалось что ничего не изменилось, а кое-где стало лучше:

      • точно знаю какой java-код выполняется

      • точно знаю какие SQL запросы идут в базу и имею полную свободу их оптимизации

      • Слой общения с БД теперь по-настоящему отделён от слоя логики

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

      Но большинство бэкендов всё же именно как у меня, и больно смотреть как ради генерации маппинга тянут весь Hibernate.