select
절에 대상을 지정하는 것을 프로젝션이라고 한다.
@Test
void simpleProjection() {
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있다. 프로젝션 대상이 둘 이상이면 튜플이나 DTO
로 조회한다.
프로젝션 대상이 둘 이상일 때 사용
@Test
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);
}
}
@Test
void findDtoByJPQL() {
List<MemberDto> result = em.createQuery("select new study.querydsl.dto.MemberDto(m.username, m.age) from Member m", MemberDto.class)
.getResultList();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
순수 JPA에서 DTO를 조회하려면 new study.querydsl.dto.MemberDto(m.username, m.age)
이렇게 패키지부터 작성해야한다. new
명령어로 생성자를 호출하는 방식인데 문자열에서 오타가 발생할 가능성이 크다. 하지만 Querydsl은 다음과 같은 방법을 지원한다.
@Test
void findDtoBySetter() {
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class,member.username,member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
@Test
void findDtoByField() {
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class,member.username,member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
@Test
void findDtoByConstructor() {
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,member.username,member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
만약 DTO의 필드명이 다를 경우에는 일치하는 필드가 없어 매핑을 하지 못하고 null
이 반환된다. 따라서 필드의 별칭을 지정하여 매칭시켜줘야 한다.
@Test
void findUserDtoByField() {
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();
for (UserDto userDto : result) {
System.out.println("userDto = " + userDto);
}
}
ExpressionUtils.as(source,alias)
: 필드나, 서브 쿼리에 별칭 적용username.as("memberName")
: 필드에 별칭 적용@Test
void findDtoByQueryProjection() {
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username,member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
QueryProjection
은 DTO
생성자를 Q-type
으로 미리 생성한 뒤 사용한다. 가장 큰 장점은 생성자를 이용하여 생성하기 때문에 컴파일 단계에서 에러를 확인할 수 있다. 그리고 입력 필드를 IDE
를 통해서 확인할 수 있으므로 개발할 때 많은 편리함을 가져올 수 있다. 하지만 순수한 DTO
가 아니라 Querydsl
에 의존성을 가지기 때문에 도입할 때 주의를 요구한다.
@Test
void dynamicQuery_booleanBuilder() {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember1(usernameParam, ageParam);
@Test
void dynamicQuery_WhereParam() {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember2(usernameParam, ageParam);
assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember2(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) {
return ageCond != null ? member.age.eq(ageCond) : null;
}assertThat(result.size()).isEqualTo(1);
}
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();
}
@Test
void dynamicQuery_WhereParam() {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember2(usernameParam, ageParam);
assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember2(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) {
return ageCond != null ?member.age.eq(ageCond) : null;
}
where
절에서 null
값은 무시되기 때문에 동적으로 쿼리생성이 가능하다. 위처럼 조건을 메소드로 분리시키면 재사용이 가능하고 새로운 조건을 생성할 수 있다.
@Test
void bulkUpdate() {
queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(28))
.execute();
em.flush();
em.clear();
}
주의사항으로 벌크연산은 DB
에 직접 데이터를 수정한다. 하지만 JPA
의 영속성 컨텍스트의 1차 캐시에는 과거의 데이터가 들어가 있다. 영속성 컨텍스트의 데이터가 우선순위를 가지고 있기 때문에 데이터를 조회해도 1차 캐시에 존재하는 값이 조회 될 것이다. 때문에 벌크연산 뒤에는 flush
clear
를 해준다.
@Test
void bulkAdd() {
queryFactory
.update(member)
.set(member.age,member.age.add(1))
.execute();
}
@Test
void bulkDelete() {
queryFactory
.delete(member)
.where(member.age.gt(18))
.execute();
}
💡 JPQL
배치와 마찬가지로, 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에 배치 쿼리를 실행하고 나면 영속성 컨텍스트를 초기화 하는 것이 안전하다.