4 분 소요

인프런 실전! Querydsl 강의 내용 정리

프로젝션과 결과 반환 - 기본

프로젝션 : select 대상 지정

프로젝션 대상이 하나

1
2
3
4
List<String> result = queryFactory
		.select(member.username)
		.from(member)
		.fetch();
  • 프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있음
  • 프로젝션 대상이 둘 이상이라면 튜플이나 DTO로 조회

튜플 조회

프로젝션 대상이 둘 이상일 때 사용 com.querydsl.core.Tuple

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test  
public void tupleProjection() throws Exception {  
    //when  
    List<Tuple> fetch = queryFactory  
            .select(member.username, member.age)  
            .from(member)  
            .fetch();  
  
    //then  
    for (Tuple tuple : fetch) {  
        String username = tuple.get(member.username);  
        Integer age = tuple.get(member.age);  
        System.out.println("username = " + username);  
        System.out.println("age = " + age);  
    }  
}

다음과 같이 쓸 수 있다.

Tuple 은 리파지토리단 안에서만 사용하자.
밖으로 넘어갈 땐 DTO로 변환해서 넘기자.

프로젝션과 결과 반환 - DTO 조회

순수 JPA 에서 DTO 조회 방법

일단 MemberDto 생성

1
2
3
4
5
6
7
8
9
10
11
@Data  
public class MemberDto {  
  
    private String username;  
    private int age;  
  
    public MemberDto(String username, int age) {  
        this.username = username;  
        this.age = age;  
    }  
}

그 다음 순수 JPQL로 작성해 보면

1
2
3
4
5
6
7
8
9
10
@Test  
public void findDtoByJPQL() throws Exception {  
    List<MemberDto> resultList = em.createQuery("select new study.querydsl.dto.MemberDto(m.username, m.age)" +  
                    " from Member m", MemberDto.class)  
            .getResultList();  
  
    for (MemberDto memberDto : resultList) {  
        System.out.println("memberDto = " + memberDto);  
    }  
}

select new study.querydsl.dto.MemberDto(m.username, m.age)

다음과 같이 dto를 생성자로 세팅 하듯이 만들어야 함.

그럼 이렇게 select 쿼리에서도 딱 username, age만 나가게 됨. 그런데 이건 좀 .. ㅋㅋ
패키지명 다 적고 그런게 좀 그렇다.

  • 순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용해야 함.
  • DTO의 패키지 이름을 다 적어줘야 해서 지저분함
  • 생성자 방식만 지원함

Querydsl 빈 생성(Bean population)

결과를 DTO로 반환할 때 사용
다음 3가지 방법을 지원

  • 프로퍼티 접근
  • 필드 직접 접근
  • 생성자 사용

프로퍼티 접근

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test  
public void findDtoBySetter() throws Exception {  
    List<MemberDto> fetch = queryFactory  
            .select(Projections.bean(MemberDto.class,  
                    member.username,  
                    member.age))  
            .from(member)  
            .fetch();  
  
    for (MemberDto memberDto : fetch) {  
        System.out.println("memberDto = " + memberDto);  
    }  
}

다음과 같이 select 절에 Projections.bean() 을 사용해서 MemberDto.class(타입)를 불러오고 그 다음 내가 원하는 값 username, age를 차례로 적어주면 된다.

하지만 이렇게만 해놓고 돌려보면 에러가 나온다.

원인은 MemberDto에 기본 생성자를 안 만들어 줬기 때문이다.

@NoArgsConstructor 나 기본 생성자를 만들어 주자.

그 후 다시 돌려보면 정상적으로 동작한다.

필드 직접 접근

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test  
public void findDtoByField() throws Exception {  
    List<MemberDto> fetch = queryFactory  
            .select(Projections.fields(MemberDto.class,  
                    member.username,  
                    member.age))  
            .from(member)  
            .fetch();  
  
    for (MemberDto memberDto : fetch) {  
        System.out.println("memberDto = " + memberDto);  
    }  
}

Projections.fields() 얘는 Setter 필요 없이 그냥 필드 변수를 바꿔버린다.

생성자 접근 방식

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test  
public void findDtoByConstructor() throws Exception {  
    List<MemberDto> fetch = queryFactory  
            .select(Projections.constructor(MemberDto.class,  
                    member.username,  
                    member.age))  
            .from(member)  
            .fetch();  
  
    for (MemberDto memberDto : fetch) {  
        System.out.println("memberDto = " + memberDto);  
    }  
}

Projections.constructor() 이걸 사용하면 되고

여기서 주의점은 member.username, member.age 이런 걸 생성자와 동일하게 넣어줘야 한다.

기타 주의 사항

만약에 UserDto를 다음과 같이 만들었다 쳐보자.

1
2
3
4
5
@Data  
public class UserDto {  
    private String name;  
    private int age;  
}

이제 필드 주입을 해서 UserDto로 반환할 거다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test  
public void findUserDtoByField() throws Exception {  
    List<UserDto> fetch = queryFactory  
            .select(Projections.fields(UserDto.class,  
                    member.username,  
                    member.age))  
            .from(member)  
            .fetch();  
  
    for (UserDto userDto : fetch) {  
        System.out.println("userDto = " + userDto);  
    }  
}

다음과 같이 하고 돌려 보자.

보이는가? name = null 이 들어간다.

원인은 우리가 select 절에 member.username 이렇게 넣었기 때문이다. 따라서 방법은 다음과 같다.

member.username.as("name")

이렇게 수정하고 다시 돌려보자

이제 원하는 대로 동작하게 된다.

기타 주의 사항 - 2 서브 쿼리

만약에 age에 최대 값을 구하고 싶어서 서브 쿼리를 사용한다면?

서브 쿼리 as ‘age’ 뭐 이런 식으로 해야 할 것이다.

코드로 보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test  
public void findUserDtoByFieldSubQuery() throws Exception {  
    QMember memberSub = new QMember("memberSub");  
    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();  
  
    for (UserDto userDto : fetch) {  
        System.out.println("userDto = " + userDto);  
    }  
}

다음과 같이 작성해야 한다.

age 부분만 따로 보면

1
2
3
ExpressionUtils.as(JPAExpressions  
                            .select(memberSub.age.max())  
                            .from(memberSub), "age"))

ExpressionUtils.as()라는 메서드 안에 이전에 배운 서브 쿼리 생성 방법 JPAExpressions 을 사용해서 서브 쿼리를 만들고 그 후 마지막에 alias 로 "age" 를 넣어 줬다.

결과가 최대 값 40으로 잘 나왔다.

  • 프로퍼티나 필드 접근 생성 방식에서 이름이 다를 때 해결 방안
  • ExpressionUtils.as(source, alias) : 필드 혹은 서브 쿼리에 별칭 사용
  • username.as("memberName") : 그냥 필드에 별칭 사용

기타 주의 사항 - 3 생성자 주입

1
2
3
4
5
6
7
8
9
10
11
12
@Data  
@NoArgsConstructor  
public class UserDto {  
  
    private String name;  
    private int age;  
  
    public UserDto(String name, int age) {  
        this.name = name;  
        this.age = age;  
    }  
}

생성자를 만들어 놓고

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test  
public void findUserDtoByConstructor() throws Exception {  
    List<UserDto> fetch = queryFactory  
            .select(Projections.constructor(UserDto.class,  
                    member.username,  
                    member.age))  
            .from(member)  
            .fetch();  
  
    for (UserDto userDto : fetch) {  
        System.out.println("userDto = " + userDto);  
    }  
}

다음 코드를 돌려 보면

잘 된다. 이름은 굳이 상관 없고, 생성자의 타입 이랑 순서 가 중요하기 때문에

1
2
3
4
public UserDto(String name, int age) {  
	this.name = name;  
	this.age = age;  
} 

여기에 맞게 잘 들어 간다.

결론

  • 프로퍼티 접근, 필드 접근은 이름을 항상 맞춰줘야 한다.
  • 생성자 접근은 이름은 맞출 필요 없지만, 기본 생성자가 있어야 하고, 순서와 타입을 잘 맞추자.

태그: ,

카테고리:

업데이트:

댓글남기기