Есть несколько способов составить JPA-запрос, среди них — неудобный JPA Criteria API. Все признают, что запросы, составленные таким способом, плохо читаются. Как альтернатива появилась библиотека QueryDSL — она гораздо удобнее.
Попробуем добавить ее в Spring Boot приложение.
Maven-зависимости и плагин
Для начала в проект нужно включить зависимости:
<dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-apt</artifactId> <version>${querydsl.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.querydsl</groupId> <artifactId>querydsl-jpa</artifactId> <version>${querydsl.version}</version> </dependency>
Также добавляем плагин:
<plugin> <groupId>com.mysema.maven</groupId> <artifactId>apt-maven-plugin</artifactId> <version>1.1.3</version> <executions> <execution> <goals> <goal>process</goal> </goals> <configuration> <outputDirectory>target/generated-sources</outputDirectory> <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> </configuration> </execution> </executions> </plugin>
При компиляции плагин сгенерирует классы, название которых начинается с буквы «Q» — по одному классу для каждой сущности (@Entity). Эти классы предоставляют «пути» для каждого поля сущности; с помощью них легко составить читаемый запрос.
Модель
Например, у нас есть сущности User и Post, для них сгенерируются классы QUser и QPost:
User:
@Getter @Setter @NoArgsConstructor @Entity public class User { @Id @GeneratedValue(generator = "sequence") private Long id; private String email; private String nickname; private String password; private String role = "ROLE_USER"; private boolean locked = false; @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "user") private Set<Post> posts = new HashSet<>(); }
Класс Post:
@Entity @Data @NoArgsConstructor public class Post { @Id @GeneratedValue(generator = "sequence") private Long id; private String title; private String text; @ManyToOne private User user; }
Сгенерированный плагином класс QPost:
@Generated("com.querydsl.codegen.DefaultEntitySerializer") public class QPost extends EntityPathBase<Post> { ... public static final QPost post = new QPost("post"); public final NumberPath<Long> id = createNumber("id", Long.class); public final StringPath text = createString("text"); public final StringPath title = createString("title"); public final QUser user; ... }
Составим запрос с помощью QPost. Экземпляр его берется так:
QPost post=QPost.post;
Далее в ход идет
com.querydsl.jpa.impl.JPAQueryFactory
Примеры select
Запрос для получения постов по заголовку и пользователю выглядит так:
JPAQueryFactory queryFactory=new JPAQueryFactory(entityManager); QPost post=QPost.post; List<Post> posts=queryFactory.selectFrom(post) .where(post.user.id.eq(1l).and(post.title.like("S%"))) .fetch();
Здесь мы выбираем посты пользователя с id=1, у которых заголовок начинается с буквы «S».
Составим более сложный запрос — с join fetch — тут вместе с постами извлекаем все данные пользователей:
JPAQueryFactory queryFactory=new JPAQueryFactory(entityManager); QPost post=QPost.post; QUser user=QUser.user; List<Post> posts=queryFactory.selectFrom(post).innerJoin(post.user, user).fetchJoin() .where(post.user.id.eq(id).and(post.title.like(title))) .fetch();
Генерируемый SQL:
select post0_.id as id1_0_0_, user1_.id as id1_1_1_, post0_.text as text2_0_0_, post0_.title as title3_0_0_, post0_.user_id as user_id4_0_0_, user1_.email as email2_1_1_, user1_.locked as locked3_1_1_, user1_.nickname as nickname4_1_1_, user1_.password as password5_1_1_, user1_.role as role6_1_1_ from post post0_ inner join user user1_ on post0_.user_id=user1_.id where post0_.user_id=? and (post0_.title like ? escape '!')
А теперь наоборот, выберем пользователей с постами ( используем left join, чтобы выбрать в том числе пользователей, у которых нет постов):
public List<User> findWithJoin(String title) { JPAQueryFactory queryFactory=new JPAQueryFactory(entityManager); QUser user= QUser.user; List<User> users = queryFactory.selectFrom(user) .leftJoin(user.posts).fetchJoin().distinct() .fetch(); return users; }
distinct() нужен, чтобы убрать из коллекции дубликаты (статья про distinct). Удаление дубликатов выполняется уже после извлечения данных из базы.
Генерируемый SQL:
select distinct user0_.id as id1_1_0_, posts1_.id as id1_0_1_, user0_.email as email2_1_0_, user0_.locked as locked3_1_0_, user0_.nickname as nickname4_1_0_, user0_.password as password5_1_0_, user0_.role as role6_1_0_, posts1_.text as text2_0_1_, posts1_.title as title3_0_1_, posts1_.user_id as user_id4_0_1_, posts1_.user_id as user_id4_0_0__, posts1_.id as id1_0_0__ from user user0_ left outer join post posts1_ on user0_.id=posts1_.user_id
Пример update
update таблицы с помощью QueryDSL тоже можно сделать. Ниже обновляется title тех постов заданного пользователя, у которых старый title такой-то.
public long updatePosts(String oldTitle, String title, long userId){ JPAQueryFactory queryFactory=new JPAQueryFactory(entityManager); QPost post=QPost.post; QUser user=QUser.user; return queryFactory.update(post) .where(post.user.id.eq(userId).and(post.title.like(oldTitle))) .set(post.title, title) .execute(); }
Пример delete
Наконец, рассмотрим пример удаления постов пользователя с конкретным id:
public long deletePosts( long userId){ JPAQueryFactory queryFactory=new JPAQueryFactory(entityManager); QPost post=QPost.post; QUser user=QUser.user; return queryFactory.delete(post) .where(post.user.id.eq(userId)) .execute(); }
Итоги
QueryDSL-запросы читать и составлять легко (при условии знания JPQL).
Код примера доступен на GitHub.
В Spring Boot есть также QuerydslPredicateExecutor — этим классом можно расширить репозиторий и составлять QueryDSL-предикаты. Иначе говоря, динамически формировать критерии для select. Подробнее тут.