QueryDSL 사용하기

By | 2022년 7월 23일
Table of Contents

QueryDSL 사용하기

QueryDSL 5.0.0 으로 테스트 되었습니다.

테이블 생성

CREATE DATABASE db_test;

USE db_test;

SHOW TABLES;

DROP TABLE tbl_user;
DROP TABLE tbl_team;

CREATE TABLE tbl_user (
    id int NOT NULL AUTO_INCREMENT,
    team varchar(32) NULL,
    username varchar(32),
    email varchar(32),
    PRIMARY KEY (id)
)
COLLATE='utf8_general_ci'
ENGINE=INNODB;

CREATE TABLE tbl_team (
    id varchar(32) NULL,
    teamname varchar(32),
    PRIMARY KEY (id)
)
COLLATE='utf8_general_ci'
ENGINE=INNODB;

INSERT INTO tbl_team(id, teamname)
VALUES('001', '개발팀');

INSERT INTO tbl_team(id, teamname)
VALUES('002', '운영팀');

INSERT INTO tbl_user(team, username, email)
VALUES('001', 'Lee', 'skyer9@gmail.com');

INSERT INTO tbl_user(team, username, email)
VALUES('005', 'Lee2', 'skyer9@gmail.com');

QClass 명

Entity 명과 필드명이 일치하는 경우,
QClass 명이 team1 과 같이 숫자가 붙는다.

@Getter
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "tbl_team", catalog = "db_test")
public class Team {

    @Id
    private String team;

    private String teamname;
}

위에서 클래스명과 필드명이 동일하므로,
QClass 는 team1 이 된다.

import static com.example.demo.domain.QTeam.team1;

join, leftJoin, eq

    public List<User> join() {

        return jpaQueryFactory
                .selectFrom(user)
                .join(team)
                    .on(user.team.eq(team.id))
                .where(team.teamname.eq("개발팀"))
                .fetch();
    }

    public List<User> leftJoin() {

        return jpaQueryFactory
                .selectFrom(user)
                .leftJoin(team)
                    .on(user.team.eq(team.id))
                .where(team.teamname.eq("개발팀"))
                .fetch();
    }

    List<SearchUserResponseDto> users = jpaQueryFactory
            .select(
                    Projections.bean(
                            SearchUserResponseDto.class,
                            user.companyId,
                            user.userId,
                            company.companyName
                    )
            )
            .from(user)
            .leftJoin(company)
            .on(user.companyId.eq(company.companyId))

join subquery

참조

JPQL 이 join subquery 를 지원하지 않기에,
QueryDsl 도 서브쿼리를 사용할 수 없다.

아래의 방법으로 hibernate 레벨에서 제공하는 서브 쿼리를 이용할 수 있다.
(native query 를 사용하므로 DB 교체시 고려해야 한다.)

@Entity
@Getter
@Setter
@Subselect("select * from tbl_user where username in ('Lee', 'Lee2')") // native query
public class SubUser {

    @Id
    private Integer id;

    private String team;

    private String username;

    private String email;
}
import static com.example.demo.domain.QTeam.team;
import static com.example.demo.domain.QUser.user;
import static com.example.demo.domain.QSubUser.subUser;

// ......

    public List<User> joinSubQuery() {

        return jpaQueryFactory
                .selectFrom(user)
                .join(subUser)
                    .on(user.id.eq(subUser.id))
                .where(subUser.username.eq("Lee"))
                .fetch();
    }

from subquery

JPQL 이 from subquery 를 지원하지 않기에,
QueryDsl 도 서브쿼리를 사용할 수 없다.

native query 로 작성하거나,
애플리케이션 레벨에서 처리해야 한다.

>, >=, <, <=, between

gt == greater then, goe == greater or equal
lt == lower then, loe == lower or equal

        return jpaQueryFactory
                .selectFrom(user)
//                .where(user.id.gt(1))
//                .where(user.id.goe(1))
//                .where(user.id.lt(1))
//                .where(user.id.loe(1))
                .where(user.id.between(1, 2))
                .fetch();

or

    public List<User> or() {

        return jpaQueryFactory
                .selectFrom(user)
                .join(team)
                    .on(user.id.eq(user.id))
                .where(
                        team.teamname.eq("개발팀")
                        .or
                        (team.teamname.eq("운영팀"))
                )
                .where(user.username.eq("Lee"))
                .fetch();
    }

orQueryDelegate 를 섞어쓰면 오류가 발생한다.
첫번째 조건이 null 이되면 NullPointerException 이 발생한다.

not

ne == not equal

        return jpaQueryFactory
                .selectFrom(user)
                .join(team)
                    .on(user.team.eq(team.id))
                .where(team.teamname.ne("운영팀"))
                .where(user.username.eq("Lee"))
                .fetch();

is null

        return jpaQueryFactory
                .selectFrom(user)
                .leftJoin(team)
                    .on(user.team.eq(team.id))
                .where(team.teamname.isNull())
                .where(user.username.eq("Lee2"))
                .fetch();

is not null

        return jpaQueryFactory
                .selectFrom(user)
                .leftJoin(team)
                    .on(user.team.eq(team.id))
                .where(team.teamname.isNotNull())
                .where(user.username.eq("Lee"))
                .fetch();

ifnull(isnull)

대체 필드도 가능하고, 대체 변수도 가능하다.
필드 또는 변수는 여러개도 가능하다.
여러개인 경우 null 이 아닌 첫번째 아이템이 반환된다.
모두 null 이면 null 이 반환된다.

    public List<User> isnull() {
        return jpaQueryFactory
                .selectFrom(user)
                .where(
                        //user.username.coalesce(user.email).eq("Lee")
                        user.username.coalesce("Lee").eq("Lee")
                )
                .fetch();
    }

in

        List<String> usernames = Arrays.asList("Lee", "Lee2");
        return jpaQueryFactory
                .selectFrom(user)
                .where(user.username.in(usernames))
                .fetch();

in subquery

서브쿼리는 언제나 성능이 떨어지니 다른 방법을 찾아보자.

        return jpaQueryFactory
                .selectFrom(user)
                .where(user.username.in(
                        JPAExpressions
                                .select(user.username)
                                .from(user)
                                .where(user.username.in("Lee", "Lee2"))))
                .fetch();

like

like() 는 % 를 넣어주어야 한다.

        return jpaQueryFactory
                .selectFrom(user)
                // .where(user.username.startsWith("Lee"))
                .where(user.username.contains("Lee"))
                .fetch();

        return jpaQueryFactory
                .selectFrom(user)
                .where(user.username.like("Lee%"))
                .fetch();

case when

    public List<String> caseWhen() {

        return jpaQueryFactory
                .select(
                        user.username
                                .when("Lee").then("A")
                                .when("Lee2").then("B")
                                .otherwise("C")
                                .as("username")
                )
                .from(user)
                .where(user.id.between(1, 2))
                .fetch();
    }

sum/min/max/count + case when

CREATE TABLE `tbl_commute_log` (
    `yyyymmdd` VARCHAR(10) NOT NULL COLLATE 'utf8mb4_general_ci',
    `user_id` VARCHAR(32) NOT NULL COLLATE 'utf8mb4_general_ci',
    `inout_type` TINYINT(4) NOT NULL DEFAULT '0',
    `act_time` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00'
)
COLLATE='utf8mb4_general_ci'
ENGINE=InnoDB

INSERT INTO tbl_commute_log(yyyymmdd, user_id, inout_type, act_time)
VALUES
('2022-11-01', 'skyer9', 1, '2022-11-01 09:01:00'),
('2022-11-01', 'skyer9', 3, '2022-11-01 10:01:00'),
('2022-11-01', 'skyer9', 4, '2022-11-01 11:01:00'),
('2022-11-01', 'skyer9', 5, '2022-11-01 12:01:00'),
('2022-11-01', 'skyer9', 2, '2022-11-01 18:01:00')

case 문을 사용하는 대신 self join 을 이용해 해결할 수 있다.

    @Override
    public List<CommuteLogStatDto> getStat() {
        QCommuteLog T1 = new QCommuteLog("T1");
        QCommuteLog T2 = new QCommuteLog("T2");
        QCommuteLog T3 = new QCommuteLog("T3");
        return jpaQueryFactory
                .select(
                        Projections.bean(
                                CommuteLogStatDto.class
                                , T1.id.yyyymmdd
                                , T1.id.userId
                                , T1.id.yyyymmdd.count().as("inoutCount")
                                , T2.actTime.min().as("minInTime")
                                , T3.actTime.max().as("maxInTime")
                        )
                )
                .from(T1)
                .leftJoin(T2)
                .on(
                        T1.id.yyyymmdd.eq(T2.id.yyyymmdd)
                        .and(T1.id.userId.eq(T2.id.userId)
                        .and(T2.id.inoutType.intValue().eq(1)))
                )
                .leftJoin(T3)
                .on(
                        T1.id.yyyymmdd.eq(T3.id.yyyymmdd)
                        .and(T1.id.userId.eq(T3.id.userId)
                        .and(T3.id.inoutType.intValue().eq(2)))
                )
                .groupBy(T1.id.yyyymmdd, T1.id.userId)
                .orderBy(T1.id.yyyymmdd.asc(), T1.id.userId.asc())
                .fetch();
    }

dynamic query

간단한 조건인 경우

    public List<User> dynamicQuery(String username, String teamname) {

        return jpaQueryFactory
                .selectFrom(user)
                .join(team)
                    .on(user.team.eq(team.id))
                .where(
                        (username != null) ? user.username.eq(username) : null,
                        (teamname != null) ? team.teamname.eq(teamname) : null
                )
                .fetch();
    }

QueryDelegate

QClass 를 재컴파일해야 인식한다.
where 절이 아주 깔끔해진다.

첫번째 파라미터는 @QueryDelegate(User.class) 걸린
Entity 의 QClass 여야 한다.

@QueryEntity
public class UserExpression {

    @QueryDelegate(User.class)
    public static BooleanExpression eqUsernameLee(QUser user) {

        return user.username.eq("Lee");
    }

    @QueryDelegate(User.class)
    public static BooleanExpression eqUsername(QUser user, String username) {

        if (username == null) {
            return null;
        }

        return user.username.eq(username);
    }
}
    @Override
    public List<User> querydelegate() {
        return jpaQueryFactory
                .selectFrom(user)
                .where(
                        user.eqUsernameLee(),
                        user.eqUsername("Lee")
                )
                .fetch();
    }

두개 이상의 Entity 에 조건을 걸려면,
아무 Entity 에 QueryDelegate 붙이고,
나머지 Entity 를 파라미터로 전달하면 된다.

@QueryEntity
public class UserExpression {
    @QueryDelegate(UserEntity.class)
    public static BooleanExpression departmentEquals(QUserEntity userEntity, QDepartmentEntity departmentEntity, Integer departmentId, String incSubDepartment) {

        if (departmentId == null) {
            return null;
        }

        if ("N".equals(incSubDepartment)) {
            return userEntity.departmentId.eq(departmentId);
        } else {
            return departmentEntity.cId1.coalesce(-1).eq(departmentId);
        }
    }
}

order by

    public List<User> orderBy() {

        return jpaQueryFactory
                .selectFrom(user)
                .where(user.username.contains("Lee"))
                .orderBy(user.username.asc(), user.id.asc())
                // .orderBy(user.username.desc())
                .fetch();
    }

group by

Projections 의 객체 생성방법 3가지 에 대해 먼저 읽으시면 좋습니다.

아래 샘플코드는 Setter 를 이용한 방식입니다.

@Getter
@Setter
@NoArgsConstructor
public class UserGroupByDto {

    private String username;
    private Long count;

    private UserGroupByDto(String username, Long count) {
        throw new RuntimeException("Projections 을 이용해 객체 생성할 것");
    }
}
    public List<UserGroupByDto> groupBy() {

        return jpaQueryFactory
                .select(
                        Projections.bean(
                                UserGroupByDto.class
                                , user.username
                                , user.count().as("count")
                        )
                )
                .from(user)
                .groupBy(user.username)
                .orderBy(user.username.asc())
                .fetch();
    }
    @Test
    void groupBy() {
        // given

        // when
        List<UserGroupByDto> list = repository.groupBy();

        // then
        assertTrue(list.size() > 0);

        UserGroupByDto item = list.get(0);
        Assertions.assertEquals(item.getUsername(), "Lee");
        Assertions.assertEquals(item.getCount(), 3);
    }

union, union all

JPA 에서는 union 을 지원하지 않는다.

"com.querydsl:querydsl-sql:5.0.0" 을 이용해 Native SQL 로 작성하거나,
hibernate 레벨에서 native query 를 작성해야 한다.
(어느 경우든 native query 를 사용하므로 DB 교체시 고려해야 한다.)

paging

PageableExecutionUtils 를 사용하면,
count 쿼리가 필요없을 때 쿼리를 생략한다.

    public Page<User> paging(Pageable pageable) {

        // 'fetchResults()' is deprecated

        List<User> list = jpaQueryFactory
                .selectFrom(user)
                .where(user.username.like("Lee%"))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        JPAQuery<User> count = jpaQueryFactory
                .selectFrom(user)
                .where(user.username.like("Lee%"));

        return PageableExecutionUtils.getPage(list, pageable, () -> count.fetch().size());
    }
    @Test
    void paging() {
        // given
        Pageable pageable = PageRequest.of(0, 2);

        // when
        Page<User> page = repository.paging(pageable);

        // then
        List<User> list = page.toList();

        assertEquals(page.getTotalElements(), 3);
        assertEquals(list.size(), 2);
    }

답글 남기기