Введение в QueryDSL

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

При компиляции плагин сгенерирует классы, название которых начинается с буквы « — по одному классу для каждой сущности (@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. Подробнее тут.

Добавить комментарий

Ваш адрес email не будет опубликован.