QueryDSL(3) - 프로젝션, distinct, 동적 쿼리, 벌크 연산
프로젝션
프로젝션과 기본 결과 반환
프로젝션 대상이 하나인 경우
@Test
public void simpleProjection() {
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
}
- 타입을 명확하게 지정할 수 있음.
- 프로젝션 대상이 둘 이상이면 Tuple이나 DTO로 조회해야함
프로젝션 대상이 둘 이상일 경우
Tuple로 조회
@Test
public void tupleProjection() {
List<Tuple> result = queryFactory
.select(member.username, member.age)
.from(member)
.fetch();
for (Tuple tuple : result) {
String username = tuple.get(member.username);
Integer age = tuple.get(member.age);
System.out.println("username = " + username);
System.out.println("age = " + age);
}
}
- com.querydsl.core.Tuple로 결과를 반환 받을 수 있음
- get()에 쿼리 타입(Q)으로 alias를 주입해 값을 반환 받을 수 있다
예: tuple.get(member.username)
참고! 투플 같은 경우에는 레포지토리 계층에서 사용하는 것은 괜찮으나, 그 하부(서비스, 컨트롤러) 계층으로 내려가게 되면 좋은 설계가 아니다. 앞단의 핵심 비즈니스 로직이 QueryDSL의 의존성으로 엮여버리면 좋지 않다. QueryDSL의 의존성이 앞단까지 넘어가기 때문에 좋지 않음.
DTO 조회
회원 엔티티 DTO 예시
import lombok.Data;
@Data
public class MemberDto {
private String username;
private int age;
public MemberDto() {
}
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
QueryDSL에서 결과를 DTO로 조회할 때 빈 생성 방법 3가지를 지원한다.
- 프로퍼티 접근: setter를 이용하여 값을 주입
- 필드 직접 접근
- 생성자 사용
// setter를 이용하여 주입
@Test
public void findDtoBySetter() {
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
}
// 필드에 바로 값을 꼽아버림
@Test
public void findDtoByField() {
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
}
// 생성자를 이용하여 주입
@Test
public void findDtoByConstructor() {
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
}
- 프로퍼티 접근이나 필드 직접 접근 시 DTO의 필드명과 엔티티의 속성명을 맞춰줘야함.
- 생성자 사용의 경우에는 순서와 타입만 맞춰주면 된다.
프로퍼티 접근이나 필드 직접 접근 시 이름이 다를 때 해결 방안
@Test
public void findUserDto() {
QMember memberSub = new QMember("memberSub");
List<UserDto> result = queryFactory
.select(Projections.bean(UserDto.class,
member.username.as("name"),
// 서브 쿼리를 이용하는 방법 ExpressionUtils로 감싸줘야 한다.
ExpressionUtils.as(JPAExpressions
.select(memberSub.age.max())
.from(memberSub), "age")))
.from(member)
.fetch();
}
- ExperssionUtils.as(source, alias): 필드나 서브 쿼리에 별칭 적용
- username.as("memberName"): 필드에 별칭 적용
@QueryProjection 사용하여 DTO 생성자 사용하여 조회
생성자 + @QueryProjection
@Data
public class MemberDto {
private String username;
private int age;
public MemberDto() {
}
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
- 사용할 생성자에 @QueryProjection 어노테이션을 붙여주면 됨.
- 어노테이션을 붙였다면 Q타입으로 DTO를 생성하여 사용할 수 있다.
@QueryProjection 활용 예시
@Test
public void findDtoByQueryProjection() {
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
}
- 생성자 파라미터 타입만 맞춰주면 잘 주입된다.
@QueryProjection는 QueryDSL에서 제공하는 프로젝션 궁극의 방법이지만 장단점이 존재한다.
장점
- Projections.constructor()를 사용하면 컴파일 오류를 잡을 수 없지만, 해당 방법은 컴파일 오류로 잡을 수 있다.
단점
- Q파일을 생성해야함.
- 의존관계가 문제, DTO 자체가 QueryDSL에 의존성이 생김. 그럼 컨트롤러, 서비스 계층에서 QueryDSL의 의존성이 생기기 때문에 DTO가 순수함을 잃어버린다. 아키텍처 설계관점에서 좋진 않다.
@QueryProjection 결론! 실용적인 관점에서는 사용하면 좋지만, 서비스의 운영 시점에서 봤을 때는 고려해볼 사항이다.
Distinct
List<String> result = queryFactory
.select(member.username).distinct()
.from(member)
.fetch();
- JPQL의 distinct와 같은 기능을 제공한다.
동적 쿼리
동적 쿼리를 사용하면 특정 조건에 따른 쿼리를 편리하게 생성할 수 있다.
동적 쿼리를 작성하는 방식에는 두 가지가 있다.
- BooleanBuilder 활용
- where()에 다중 파라미터 사용
BoolenBuilder 활용 예시
@Test
public void dynamicQuery_BooleanBuilder() {
String usernameParam = "member1";
int ageParam = 10;
List<Member> result = searchMember1(usernameParam, ageParam);
}
public 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 인스턴스에 .and()를 사용해 검색 조건을 붙일 수 있다.
where 다중 파라미터 활용 예시
@Test
public void dynamicQuery_WhereParam() {
String usernameParam = "member1";
int ageParam = 10;
List<Member> result = searchMember(usernameParam, ageParam);
}
public List<Member> searchMember(String usernameCond, Integer ageCond) {
return queryFactory
.selectFrom(member)
.where(usernameEq(usernameCond), ageEq(ageCond))
.fetch();
}
// 아래와 같이 동적 조건 쿼리를 메소드로 빼면 재사용성이 증가한다. (다른 쿼리에서 재사용이 가능함)
private Predicate usernameEq(String usernameCond) {
return usernameCond != null ? member.username.eq(usernameCond) : null;
}
private Predicate ageEq(Integer ageCond) {
if (ageCond == null) {
return null;
}
return member.age.eq(ageCond);
}
- where 조건에 null 값이 있으면 무시되는데 이를 활용함.
- 메소드를 따로 빼서 생성하면 다른 쿼리에서도 재사용할 수 있음.
- 쿼리 자체의 가독성이 높아짐.
위의 동적 조건 쿼리 메소드를 아래와 같이 조합하여 하나의 메소드로 만들 수도 있다.
private BooleanExpression allEq(String usernameCond, Integer ageCond) {
return usernameEq(usernameCond).and(ageEq(ageCond));
}
- BooleanExpression: Predicate을 상속 받아 구현된 추상 클래스
- BooleanExpression를 이용하면 and()를 체인으로 한 번에 동적 쿼리를 엮을 수 있다.
- 단 이 경우에는 null 값 처리를 잘 해주어야 한다.
수정, 삭제 시 벌크 연산
수정
@Test
public void bulkUpdate() {
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(14))
.execute();
}
- 반환되는 long 타입 값은 반영된 데이터의 수
아래와 같이 기존 값에 더하기, 곱 연산이 가능하다.
// 더하기 (마이너스 시 음수 주입)
@Test
public void bulkAdd() {
long count = queryFactory
.update(member)
.set(member.age, member.age.add(2))
.execute();
}
// 곱
@Test
public void bulkMultiply() {
long count = queryFactory
.update(member)
.set(member.age, member.age.multiply(2))
.execute();
}
삭제
@Test
public void bulkDelete() {
long count = queryFactory
.delete(member)
.where(member.age.gt(13))
.execute();
}
주의! JPQL 배치 쿼리와 마찬가지로, 영속성 컨텍스트에 있는 엔티티를 무시하고 데이터베이스에 쿼리를 날리기 때문에 배치 쿼리를 실행한 후에는 영속성 컨텍스트를 초기화 하는 것이 안전하다.