김영한 님의 실전! Querydsl 강의를 보고 작성한 내용입니다.
프로젝션 대상이 하나라면 타입을 명확히 지정할 수 있습니다.
대상이 둘 이상인 경우 튜플이나 DTO 를 통해 반환 받게 됩니다.
List<MemberDto> result = em.createQuery(
"select new study.querydsl.dto.MemberDto(m.username, m.age) " +
"from Member m", MemberDto.class)
.getResultList();
순수 JPA 에서 DTO 를 조회할 때는 new 명령어를 사용해서 생성자를 통해 반환합니다. 또한 DTO 의 패키지 경로까지 모두 작성해야 합니다.
QueryDSL 에서 DTO 를 쉽게 조회할 수 있도록 빈 생성 방식을 제공합니다. 이때 3가지 방법으로 DTO 에 값을 담아 반환할 수 있습니다.
프로퍼티 접근( setter )
필드 직접 접근
생성자 사용
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
Projections.bean()
을 사용해서 DTO 를 바로 조회할 수 있습니다. 첫 번째 파라미터로 어떤 DTO 인지를 명시하고, 그 뒤에 필요한 필드들을 넣어주면 됩니다. 이때 DTO 에는 기본 생성자가 필요합니다.
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
Projections.fields
를 통해 필드에 값을 바로 넣어버리기 때문에 DTO 에 getter, setter 는 필요하지 않습니다. 형태는 프로퍼티 접근과 동일합니다.
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
Projections.constructor
를 통해 생성자 접근 방식으로 DTO 를 생성할 수 있습니다. 이때 전달하는 username 과 age 는 DTO 에 선언된 username, age 와 타입이 일치해야 합니다.
Projection 에서 사용한 username, age 이 필드명 그대로 MemberDto 에 존재하기 때문에 사용할 수 있었습니다. 즉, 엔티티와 DTO 의 필드명이 동일하게 매칭되었기 때문에 가능했습니다.
하지만 만약 username 이 아닌 name 이라는 필드를 가진 DTO 가 있으면 어떻게 될까요?
public class UserDto {
private String name;
private int age;
}
@Test
void findUserDto() {
List<UserDto> result = queryFactory
.select(Projections.fields(UserDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (UserDto userDto : result) {
System.out.println("userDto = " + userDto);
}
}
userDto = UserDto(name=null, age=10)
userDto = UserDto(name=null, age=20)
userDto = UserDto(name=null, age=30)
userDto = UserDto(name=null, age=40)
테스트 실행 결과를 보면 username 과 name 이 매칭이 되지 않기 때문에 name 에 null 값이 들어간 것을 볼 수 있습니다.
생성자의 경우 DTO 에 생성자가 존재하면 문제없이 동작하지만, 프로퍼티나 필드 접근 생성 방식에서 이름이 다를 때는 문제가 발생하며, 이를 해결할 수 있는 방법은 2가지가 존재합니다.
@Test
void findUserDto() {
List<UserDto> result = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"),
member.age))
.from(member)
.fetch();
}
엔티티 필드에 .as("dto 필드명")
를 붙이면 정상적으로 username 이 name 필드에 들어가게 됩니다.
@Test
void findUserDto() {
QMember memberSub = new QMember("memberSub");
List<UserDto> result = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"),
ExpressionUtils.as(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub), "age")
)
)
.from(member)
.fetch();
}
ExpressionUtils.as(source, alias)
형태로 필드나 서브 쿼리에 별칭을 지정하는 형태로 이름이 다른 문제를 해결할 수 있습니다.
필드의 경우는 별칭을 사용하는 방식으로 해결하고, 서브 쿼리의 경우 ExpressionUtils 를 사용해서 해결합니다.
@Data
public class MemberDto {
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
응답으로 사용할 DTO 의 생성자에 @QueryProection
을 붙이면 Q 클래스가 생성되고, 그 내부에 생성자를 가지게 됩니다.
@Test
void findDtoByQueryProjection() {
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
}
그 후 select 절에서 new
를 통해 Q 클래스를 생성하면 기존 DTO 가 생성돼서 반환됩니다.
@QueryProjection
을 사용하면 컴파일 시점에 오류를 잡을 수 있는 반면, Projections.constructor
를 사용하면 런타임에 오류가 발생하게 됩니다.
하지만 DTO 에 QueryDSL 어노테이션을 유지해야 하는 점과 DTO 까지 Q 클래스를 생성해야 하는 단점이 있습니다.
private List<Member> searchMember1(String usernameCond, Integer ageCond) {
BooleanBuilder builder = new BooleanBuilder();
if (usernameCond != null) {
builder.and(member.username.eq(usernameCond));
}
if (ageCond != null) {
builder.and(member.age.eq(ageCond));
}
return queryFactory
.selectFrom(member)
.where(builder)
.fetch();
}
BooleanBuilder
를 생성해서 동적 쿼리를 작성할 수 있습니다. builder 에 조건을 추가할 수 있는데 null 인지 판단하는 로직을 추가하면 동적 쿼리를 작성할 수 있습니다. 그 후 where 절 안에 builder 를 넣으면 자동으로 조건이 생성됩니다.
만약 usernameCond 가 필수라면 new BooleanBuilder(member.username.eq(usernameCond))
로 작성해서 초기 조건을 지정할 수 있습니다.
또 where 절에 builder.and()
와 같이 계속해서 조건을 작성할 수 있습니다.
private List<Member> searchMember2(String usernameCond, Integer ageCond) {
return queryFactory
.selectFrom(member)
.where(usernameEq(usernameCond), ageEq(ageCond))
.fetch();
}
private BooleanExpression usernameEq(String usernameCond) {
return usernameCond == null ? null : member.username.eq(usernameCond);
}
private BooleanExpression ageEq(Integer ageCond) {
return ageCond == null ? null : member.age.eq(ageCond);
}
where 에 조건을 여러 개 나열하면 and 조건으로 들어가고, null 이 들어가면 무시됩니다.
이 방식을 사용하면 메서드를 다른 쿼리에서도 재활용 할 수 있다는 장점이 있습니다.
또 아래처럼 두 조건을 조합해서 사용할 수 있지만, null 체크에 주의해야 합니다.
private BooleanExpression allEq(String usernameCond, Integer ageCond) {
return usernameEq(usernameCond).and(ageEq(ageCond));
}
long count = queryFactory
.update(member)
.set(member.username, "성인")
.where(member.age.lt(19))
.execute();
execute()
를 사용하며, 반환값은 영향을 받은 로우의 수가 됩니다.
long count = queryFactory
.delete(member)
.where(member.age.gt(18))
.execute();
영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에 벌크연산을 실행하고 나면 영속성 컨텍스트를 초기화 하는 것이 좋습니다.
String result = queryFactory
.select(Expressions.stringTemplate(
"function('replace', {0}, {1}, {2})",
member.username, "member", "M"))
.from(member)
.fetchFirst();
member 를 M 으로 변경하는 replace 함수를 호출하는 코드입니다. SQL function 은 JPA 와 같이 Dialect 에 등록된 내용만 호출할 수 있습니다.