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();
나중에 다루도록 하겠다,,,