![[실전! Querydsl] 2. 중급 문법](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgGwsB%2FbtsDooO5pUU%2FWYuctHybOpONWlYDnxy4HK%2Fimg.png)
- 📌 프로젝션과 결과 반환
- ✅ 프로젝션 대상이 하나일 때
- ✅ 튜플 조회
- ✅ DTO 조회 : 순수 JPA
- ✅ DTO 조회 : Querydsl
- ✅ 프로퍼티 접근 : Setter
- ✅ 필드 접근
- ✅ 실제 엔티티의 필드 이름과 DTO의 필드 이름이 다를 때
- ✅ 생성자 접근
- 📌 Projection과 결과 반환 : @QueryProjection
- ✅ 생성자 + @QueryProjection
- ✅ 참고 : distinct
- 📌 동적 쿼리
- ✅ BooleanBuilder를 활용한 동적 쿼리
- ✅ Where 다중 파라미터를 활용한 동적 쿼리
- 📌 벌크 연산
- ✅ 벌크 연산 : 수정
- ✅ 벌크 연산 : 삭제
인프런 김영한 강사님의 [실전! Querydsl] 을 수강하고 정리한 글입니다.
실전! Querydsl 강의 - 인프런
Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!, 복잡한 쿼리, 동적 쿼리는 이제 안녕! Querydsl로 자바 백엔드 기술을 단단하게. 🚩 본 강의는 로드맵 과정입니다. 본 강의는 자바 백엔
www.inflearn.com
📌 프로젝션과 결과 반환
✅ 프로젝션 대상이 하나일 때
프로젝션(Projection)이란?
: 데이터베이스 쿼리 결과에서 원하는 필드만 선택하여 가져오는 것을 의미한다.
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
위 처럼 프로젝션의 대상이 하나라면 타입을 명확하게 지정할 수 있다.
둘 이상이면 어떨까? 둘의 타입이 서로 다르다면? => 튜플 또는 DTO를 활용하는 방안이 있다.
✅ 튜플 조회
프로젝션의 대상이 둘 이상일 때 사용하는 방식.
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);
}
위 사례에서는 username과 age를 조회 결과로서 얻고 싶은데 username은 String 타입이고 age는 Integer 타입이다.
이 문제를 해결해주는 것이 바로 Tuple 이다.
✅ DTO 조회 : 순수 JPA
기존 순수 JPA에서 DTO를 조회하는 코드는 어땠을까?
List<MemberDto> result = em.createQuery(
"select new study.querydsl.dto.MemberDto(m.username, m.age) " +
"from Member m", MemberDto.class)
.getResultList();
얼핏 봐도 기존에는 new 연산자를 활용하여 DTO의 패키지 이름을 다 적어줬어야 했기 때문에 너무 길고 복잡하고 불편했다.
그리고 생성자를 활용한 방식만 지원했었다.
이를 Querydsl을 사용하면 더 편리해진다.
✅ DTO 조회 : Querydsl
Bean Population (Querydsl 빈 생성) : 결과를 DTO로 반환할 때 사용
순수 JPA는 생성자 접근 방식만 지원했지만, Querydsl은 세 가지 접근 방식을 지원한다.
1. 프로퍼티 접근
2. 필드 접근
3. 생성자 접근
✅ 프로퍼티 접근 : Setter
List<MemberDto> result = queryFactory
.select(Projections.bean(
MemberDto.class,
member.username,
member.age
))
.from(member)
.fetch();
Projections.bean() : Setter를 활용하는 방식
✅ 필드 접근
List<MemberDto> result = queryFactory
.select(Projections.fields(
MemberDto.class,
member.username,
member.age
))
.from(member)
.fetch();
Projections.fields() : Querydsl이 필드에 직접 접근하여 DTO를 만든다.
✅ 실제 엔티티의 필드 이름과 DTO의 필드 이름이 다를 때
실제 엔티티의 필드 이름과 DTO의 필드 이름이 다를 때가 있다.
아래가 DTO의 예시라고 하자.
package study.querydsl.dto;
import lombok.Data;
@Data
public class UserDto {
private String name;
private int age;
}
이 때 (별칭이 다를 때) Querydsl은 어떻게 작성해야 할까?
List<UserDto> fetch = 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) : 필드 또는 서브 쿼리에 별칭을 적용할 때 사용한다.
username.as("memberName") : 필드에 직접 별칭을 적용할 때 사용한다.
✅ 생성자 접근
List<MemberDto> result = queryFactory
.select(Projections.constructor(
MemberDto.class,
member.username,
member.age
))
.from(member)
.fetch();
}
Projections.constructor() : 생성자 접근 시 사용하는 명령어
📌 Projection과 결과 반환 : @QueryProjection
✅ 생성자 + @QueryProjection
package study.querydsl.dto;
import com.querydsl.core.annotations.QueryProjection;
import lombok.Data;
@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 어노테이션을 붙이면 엔티티가 아님에도 불구하고 실행 시 해당 Dto의 Q파일이 생성된다.
이렇게 만든 Q 파일을 Repository에서 사용하면 편리한데, 그 예시를 보자.
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
아까처럼 Projection.xxx()를 별도로 작성할 필요는 없다.
이 방식은 컴파일러로 타입 체크가 가능하기 때문에 가장 안전한 방법이다.
그렇지만 위 어노테이션을 유지해야 한다는 점, DTO도 Q파일로 생성해야 한다는 점, 파라미터가 많아지면 코드가 복잡해진다는 점들이 있다.
✅ 참고 : distinct
List<String> result = queryFactory
.select(member.username).distinct()
.from(member)
.fetch();
JPQL의 distinct와 같다.
📌 동적 쿼리
✅ BooleanBuilder를 활용한 동적 쿼리
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를 통해 where 조건을 먼저 만든 다음에,
where() 문 안에 해당 BooleanBuilder만 깔끔하게 삽입하면 된다.
✅ Where 다중 파라미터를 활용한 동적 쿼리
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 ? member.username.eq(usernameCond) : null;
}
private BooleanExpression ageEq(Integer ageCond) {
return ageCond != null ? member.age.eq(ageCond) : null;
}
이렇게 하면 메서드를 다른 쿼리에서도 재활용할 수 있고, 쿼리 자체의 가독성이 크게 올라간다.
where 조건에 null 값은 무시된다.
📌 벌크 연산
✅ 벌크 연산 : 수정
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(28))
.execute();
위 방식으로 쿼리 하나로 대량의 데이터를 수정할 수 있다.
✅ 벌크 연산 : 삭제
long count = queryFactory
.delete(member)
.where(member.age.gt(18))
.execute();
위 방식으로 쿼리 하나로 대량의 데이터를 삭제할 수 있다.
추천
JPQL의 Batch 기능 처럼 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에
벌크 연산 후에는 영속성 컨텍스트롤 초기화 하는 것이 안전하다.
'Backend > JPA' 카테고리의 다른 글
[실전! Querydsl] 1. 기본 문법 (2) | 2024.01.11 |
---|---|
[실전! 스프링 데이터 JPA] 3. 확장 기능 (0) | 2024.01.10 |
[실전! 스프링 데이터 JPA] 2. 쿼리 메소드 기능 (1) | 2024.01.10 |
[실전! 스프링 데이터 JPA] 1. 공통 인터페이스 기능 (0) | 2024.01.05 |
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 11. 객체지향 쿼리 언어 2 (1) | 2024.01.05 |
개발자가 되고 싶어요.