JPQL запросы штука весьма удобная, почти такая же, как и SQL, только с классами и атрибутами. Но, как и у всякой удобной вещи, у неё есть недостатки, даже два:
- Запросы жёстко определяются на стадии компиляции и во время исполнения их не изменить.
- Запросы совсем никак не связаны с реальными сущностями и если сущность изменяется, то никто не скажет, что запрос больше неверен. До тех пор, пока его не попытаются выполнить.
Хорошая новость — в JPA есть механизм, который решает обе эти проблемы.
Criteria API и программно создаваемые запросы.
Примером запросов, создаваемых программно, может служить любой фильтр в любом приложении, который позволяет фильтровать данные по нескольким полям. Если рассматривать модель данных из примера с отношениями между сущностями, то можно представить себе фильтр людей по:
- Серии и номеру паспорта
- Адресу проживания
- Месту работы
- Имени
Причём фильтроваться люди могут по любой комбинации этих полей, например по адресу проживания и месту работы или по имени и адресу проживания. В терминах JPQL и SQL выразить такой запрос невозможно, потому что нет оператора «выбрать, у которых поле равно такому-то значению или игнорировать условие, если значение не установлено».
Вариантов реализации такого фильтра несколько. Можно сделать 16 различных запросов и в зависимости от того, какие значения фильтра установлены, выбирать подходящий запрос. Я даже не буду объяснять, почему это плохое решение и почему так делать никогда нельзя 😉
Можно собирать запрос из составляющих, пользуясь тем, что запрос это строка. Недостатков у этого подхода тоже много: абсолютно нечитаемый код запроса в итоге, высокая вероятность составить кривой запрос, сложность поддержания генератора запроса в актуальном состоянии и т.д.
Наконец третий и наиболее правильный вариант, это использование программно определяемых запросов и JPA Criteria API.
Начну с простого: загрузка всех сущностей заданного класса:
1
2
3
4
5
6
7
8
9
10
|
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Person> personCriteria = cb.createQuery(Person.class);
Root<Person> personRoot = personCriteria.from(Person.class);
personCriteria.select(personRoot);
em.createQuery(personCriteria)
.getResultList()
.forEach(System.out::println);
|
Создаём EntityManager, открываем транзацию и создаём CriteriaBuilder, который будет строить объекты запросов. С помощью CriteriaBuilder создаём CriteriaQuery, который параметризуется типом, который этот запрос возвращает. Затем создаётся корневой объект, от которого производится обход дерева свойств при накладывании ограничений или указании что выбирать. Последним шагом говорится, что же мы хотим выбрать и, наконец, запрос отправляется в EntityManager, где и выполняется как обычно. Построенный выше весьма многословный пример эквивалентен JPQL запросу « from Person».
Все шаги, перечисленные выше, являются обязательными для создания запроса с помощью Criteria API. Важно понимать, что корневой объект указывает JPQL, откуда будут браться данные, а CriteriaQuery указывает тип возвращаемых данных. И типы Root и CriteriaQuery могут отличаться:
1
2
3
4
5
6
|
CriteriaQuery<Passport> passportCriteria = cb.createQuery(Passport.class);
Root<Person> personPassportRoot = passportCriteria.from(Person.class);
passportCriteria.select(personPassportRoot.get("passport"));
em.createQuery(passportCriteria)
.getResultList()
.forEach(System.out::println);
|
Этот запрос аналогичен JPQL запросу « select passport from Person» и показывает, что класс, из которого запрашиваются данные и класс, который вернёт запрос, могут быть разными.
Пора переходить к самому интересному — ограничениям. Ограничения накладываются на CriteriaQuery и задаются программно, в виде вызова соответствующих функций:
1
2
3
4
5
6
7
|
CriteriaQuery<Passport> passportOwnerCriteria = cb.createQuery(Passport.class);
Root<Passport> ownerPassportRoot = passportOwnerCriteria.from(Passport.class);
passportOwnerCriteria.select(ownerPassportRoot);
passportOwnerCriteria.where(cb.equal(ownerPassportRoot.get("owner").get("lastName"), "Testoff"));
em.createQuery(passportOwnerCriteria)
.getResultList()
.forEach(System.out::println);
|
Ключевое изменение здесь конечно же:
1
|
passportOwnerCriteria.where(cb.equal(ownerPassportRoot.get("owner").get("lastName"), "Testoff"));
|
Читать это изменение удобнее от центра к краям — вначале из корневого объекта ссылаемся на поле owner, потом на поле lastName объекта owner. Метод equal() создаёт экземпляр Predicate, сравнивающий значения полученной ссылки на поле lastName и строки «Testoff». Этот Predicate отдаётся в метод CriteriaQuery.where() и теперь запрос вернёт только те объекты, для которых предикат истинен. Это аналог JPQL запроса « from Passport as p where p.owner.lastName='Testoff'».
Различных предикатов в CriteriaBuilder запасено много и рассматривать каждый смысла наверное нет, поэтому я приведу всего лишь один пример с другим предикатом:
1
2
3
4
5
6
7
|
CriteriaQuery<Passport> passportLikeCriteria = cb.createQuery(Passport.class);
Root<Passport> likePassportRoot = passportLikeCriteria.from(Passport.class);
passportLikeCriteria.select(likePassportRoot);
passportLikeCriteria.where(cb.like(likePassportRoot.get("owner").get("lastName"), "Te%"));
em.createQuery(passportLikeCriteria)
.getResultList()
.forEach(System.out::println);
|
Данный запрос аналогичен JPQL запросу « from Passport as p where p.owner.lastName like 'Te%'». Кстати, поскольку данные отправляются в предикаты как параметры функций, специальная поддержка параметров запроса становится излишней, всё происходит само по себе.
Обращение к связанным сущностям требует дополнительных усилий. Чтобы написать аналог « Select p from Person as p, IN(p.workingPlaces) as wp where wp.name = 'Acme Ltd'», необходимо явно указать связь между сущностями Company и Person:
1
2
3
4
5
6
7
8
|
CriteriaQuery<Person> personWorkCriteria = cb.createQuery(Person.class);
Root<Person> personWorkRoot = personWorkCriteria.from(Person.class);
Join<Person, Company> company = personWorkRoot.join("workingPlaces");
personWorkCriteria.select(personWorkRoot);
personWorkCriteria.where(cb.equal(company.get("name"), "Acme Ltd"));
em.createQuery(personWorkCriteria)
.getResultList()
.forEach(System.out::println);
|
Для фильтрации по полям коллекции мы вначале создаём объект связи, в котором указываем, кто с кем связан и по какому полю, а потом уже используется объект связи как источник ссылок на поля и создаём предикат для этих ссылок.
Metamodel и типобезопасность.
Все примеры выше решают проблему с программным созданием запросов, но всё ещё бессильны перед изменениями сущностей. В самом деле, изменю я в сущности Person поле workingPlaces на jobs и развалится последний запрос. И не узнаю я, что он развалится, пока не попробую его исполнить.
Metamodel решает эту проблему, создавая специальные описательные классы, которые используются в Criteria API вместо имён полей.
Сам Metamodel класс выглядит примерно вот так:
1
2
3
4
5
6
7
|
@StaticMetamodel(Company.class)
public abstract class Company_ extends AbstractIdentifiableObject_ {
public static volatile SingularAttribute<Company, String> name;
public static volatile CollectionAttribute<Company, Person> workers;
}
|
В Metamodel классе описываются, какие поля присутствуют в сущности, какого они типа, коллекция это или нет и т.д. Для каждой сущности создаётся свой класс Metаmodel.
Создаются классы Metamodel разумеется не вручную. То есть можно их и вручную создать, но тогда пропадает автоматичность проверки и теряется смысл всей этой затеи. Обычно же классы Metamodel генерируются на этапе компиляции тем или иным методом. Конкретная реализация генерации зависит от конкретной реализации JPA и может меняться.
Поскольку мы рассматриваем Hibernate реализацию JPA, то генератор будет тоже от Hibernate. Сам генератор реализован как annotation processor и может быть подключен в maven проект следующим образом:
1
2
3
4
5
6
|
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate.version}</version>
<scope>provided</scope>
</dependency>
|
Генерация классов Metamodel в этом случае будет происходить автоматически.
Использование Metamodel в JPA Criteria достаточно очевидно: везде, где можно использовать имя поля, можно использовать Metamodel:
1
2
3
4
5
6
|
CriteriaQuery<Passport> passportCriteria = cb.createQuery(Passport.class);
Root<Person> personPassportRoot = passportCriteria.from(Person.class);
passportCriteria.select(personPassportRoot.get(Person_.passport));
em.createQuery(passportCriteria)
.getResultList()
.forEach(System.out::println);
|
Или более сложный случай:
1
2
3
4
5
6
7
8
9
10
|
CriteriaQuery<Company> companyPassportCriteria = cb.createQuery(Company.class);
Root<Company> companyPassportRoot = companyPassportCriteria.from(Company.class);
Join<Company, Person> person = companyPassportRoot.join(Company_.workers);
companyPassportCriteria.select(companyPassportRoot);
companyPassportCriteria.where(cb.equal(person.get(Person_.passport).get(Passport_.series), "AS"));
em.createQuery(companyPassportCriteria)
.setFirstResult(0)
.setMaxResults(10)
.getResultList()
.forEach(System.out::println);
|
Так как Metamodel генерируется автоматически при компиляции, то комплятор знает, какие поля вы передаёте в Criteria API и какие у них типы. Следовательно, как только будет внесено изменение, ломающее запрос, компиляция этих запросов тут же провалится и разработчик сразу увидит, где ещё надо внести изменения.
Пагинация
Пагинация не относится ни к Criteria API, ни к JPQL, а работает с любыми запросами, которые создаёт метод createQuery(). Собственно речь идёт даже не о пагинации как таковой, хотя её обычно именно этими методами и делают, а о управлении количеством возвращаемых объектов и смещением от начала.
Все запросы, возвращаемые createQuery(), имеют два метода: setFirstResult() и setMaxResults().
Первый метод, setFirstResult(), говорит, сколько результатов надо отбросить от начала списка результатов при выполнении запроса, задавая смещение от начала списка.
Второй метод, setMaxResults() ограничивает количество возвращаемых объектов. Аналог этих методов в SQL — операторы LIMIT и OFFSET.
Код примера доступен на github.