В этой статье вы узнаете, как расширение 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, сохраняя свою кодовую базу чистой и удобной для сопровождения.
breninsul
Что за кошмар, дырявые абстракции все дырявее и дырявее.
"упрощая многие распространенные запрос"- просто объясните мне в чем сложность сделать обычную выборку или
jsonb_build_object(нужные поля, агрегации в массивы, что угодно )?
Вот какую задачу решают ОРМы?
Помнить про ленивую инициализацию?
Любиться с кривыми селектами в базу?
Апдейтать все поля целиком вместо инкремента 1го столбца?
Стимулировать использовать единый обьект в любых ситуациях, даже когда нам нужны всего пару полей?
mitasov-ra
Бугурт вышел немного агрессивный, но полностью согласен с каждым предложением.
ОРМ в большинстве случаев кажется абстракцией ради абстракций. Перешёл на чистый jdbc у себя в проекте, чтобы избавиться от оверхеда JPA и Hibernate, и оказалось что ничего не изменилось, а кое-где стало лучше:
точно знаю какой java-код выполняется
точно знаю какие SQL запросы идут в базу и имею полную свободу их оптимизации
Слой общения с БД теперь по-настоящему отделён от слоя логики
Мой проект если что простенький, можно сказать CRUD. Я знаю допустим что транзакции JPA могут много где пригодиться в больших сложных проектах.
Но большинство бэкендов всё же именно как у меня, и больно смотреть как ради генерации маппинга тянут весь Hibernate.