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();
}
or
와QueryDelegate
를 섞어쓰면 오류가 발생한다.
첫번째 조건이 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);
}