List<String> fetch = qf.select(member.username).from(member).fetch();
List<Member> fetch = qf.selectFrom(member).fetch();
프로젝션 대상이 하나인 경우에는 타입을 명확히 지정할 수 있다.
List<Tuple> fetch = qf.select(member.username, member.age).from(member).fetch();
for (Tuple tuple : fetch) {
String username = tuple.get(member.username);
Integer age = tuple.get(member.age);
}
프로젝션 대상이 둘 이상인 경우에는 Tuple을 사용해야 한다.
다른 계층에서는 특정 기술(QueryDSL)에 의존적이면 안 된다.
따라서 Tuple을 다른 계층에 넘기지 말고, DTO로 변환해서 전달해야 한다.
순수 JPA로 DTO를 다루려면 다음과 같을 것이다.
List<MemberDto> memberDtos = em.createQuery(
"SELECT new 패키지명.MemberDto(m.username, m.age) FROM Member m",
MemberDto.class
).getResultList();
순수 JPA로 DTO를 다룰 때의 단점
1. DTO의 패키지 명을 반드시 명시해야 한다.
2. 생성자 방식으로만 객체를 만들 수 있다.
QueryDSL에서는 세 가지 방법으로 DTO를 다룰 수 있다.
List<MemberDto> fetch = qf
.select(Projections.bean(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
기본 생성자로 객체 생성 후, Setter를 통해 값을 할당한다.기본 생성자와 Setter가 반드시 있어야 한다.List<MemberDto> fetch = qf
.select(Projections.fields(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
Setter를 사용하지 않고, 필드명을 인식하여 값을 할당한다.Member Entity의 필드명과, DTO의 필드명이 일치해야 한다.Member Entity의 필드명과, DTO의 필드명이 일치해야 한다.만약 Member Entity는 username이라는 필드명을 사용하는데, MemberDto는 name이라는 필드명을 사용하면 name필드에 null이 할당된다.
이렇게 필드명 불일치 문제를 다음과 같이 해결할 수 있다.
List<MemberDto> fetch = qf
.select(Projections.fields(MemberDto.class,
member.username.as("name"),
member.age))
.from(member)
.fetch();
DTO + Projections.field를 사용하려면 DTO 필드명 일치 여부를 주의해야 한다.
따라서 Subquery도 필드명을 일치시켜야 하는데, 다음의 방법으로 할 수 있다.
QMember subMember = new QMember("subMember")
List<MemberDto> fetch = qf
.select(Projections.fields(MemberDto.class,
// 위와 동일한 효과
ExpressionUtils.as(member.username, "username"),
ExpressionUtils.as( // subquery 결과를 필드로 사용
JPAExpressions // subquery 생성 구문
.select(subMember.age.max())
.from(subMember),
"age" // alias
))
.from(member)
.fetch();
List<MemberDto> fetch = qf
.select(Projections.constructor(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
fields와 달리 필드명은 중요하지 않다.DTO를 직접 다루는 방식은 조금 복잡하다.
@QueryProjection Annotation으로 개선할 수 있다.
@Data
@NoArgsConstructor
public class MemberDto {
private String username;
private int age;
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
@QueryProjection을 붙인다compileQuerydsl로 컴파일 하면 DTO에도 Q-Type이 생성된다.그런 뒤, 다음과 같이 사용하면 된다.
List<MemberDto> fetch = qf
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
Projections.consturctor는 컴파일 타임에 문제를 발견할 수 없다.
qf
.select(Projections.constructor(MemberDto.class,
member.username,
member.age,
member.id
))
DTO에 존재하지 않는 member.id를 사용했음에도, 컴파일 타임에 문제를 검출하지 못하고 런타임에 문제가 발생할 수도 있다.
그런데 QueryProjection은 실제 생성자를 사용하는 것이므로, 컴파일 타임에 문제를 발견할 수 있다.
QueryProjection은 QueryDSL의 기술이다.의존성 문제를 우선한다면 Projection을 사용하자
편의성을 우선한다면 그냥 QueryProjection을 사용하자.
알아서 선택하라는 뜻
QueryDSL은 두 가지 방법으로 동적 쿼리를 작성한다.
// 값이 없는 경우(null)에는 WHERE문에서 제외하고 싶은 상황
String username = "username1";
Integer age = 10;
BooleanBuilder booleanBuilder = new BooleanBuilder();
if (username != null) {
booleanBuilder.and(member.username.eq(username));
}
if (age != null) {
booleanBuilder.and(member.age.eq(age));
}
List<Member> fetch = qf
.selectFrom(member)
.where(booleanBuilder)
.fetch();
// 값이 없는 경우(null)에는 WHERE문에서 제외하고 싶은 상황
String username = "username1";
Integer age = 10;
List<Member> fetch = qf
.selectFrom(member)
.where(usernameEq(username), ageEq(age))
.fetch();
private Predicate ageEq(Integer age) {
if (age == null) {
return null;
}
return member.age.eq(age);
}
private Predicate usernameEq(String username) {
if (username == null) {
return null;
}
return member.username.eq(username);
}
WHERE의 파라미터가 NULL이면 무시된다는 특징을 활용한 방식조건 조합을 위해, Predicate보다 BooleanExpression을 반환 타입으로 사용하는 것이 좋다.
private BooleanExpression usernameEq(String username) {...}
Where 다중 파라미터 사용 권장
Dirty Checking을 사용하지 않고 DB에 직접 쿼리를 날리자.
Persistence Context 불일치에 주의해야 한다.
long count = qf
.update(member)
.set(member.username, "older than 30")
.where(member.age.gt(30))
.execute();
나중에 다루도록 하겠다,,,