Есть несколько способов составить 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. Подробнее тут.