Tools & Libraries/Querydsl

[Querydsl] Querydsl 중급문법 1 (프로젝션과 결과 반환 - Dto)

대기업 가고 싶은 공돌이 2024. 9. 11. 02:11

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

프로젝션이란 select 대상을 지정하는 것을 프로젝션이라 한다.

 

프로젝션 대상이 하나인 경우

@Test
public void simpleProjection(){
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    List<String> result = queryFactory.select(member.username)
            .from(member)
            .fetch();

    for (String s : result) {
        System.out.println("s = " + s);
    }
}

 

프로젝션 대상이 하나면 타입을 String과 같이 명확하게 지정할 수 있다.

 

만약 프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회해야 한다.

 

튜플 조회

프로젝션 대상이 둘 이상일 때 사용한다.

 

@Test
public void tupleProjection(){
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    List<Tuple> result = queryFactory.select(member.username, member.age)
            .from(member)
            .fetch();

    for (Tuple tuple : result) {
        String s = tuple.get(member.username);
        Integer i = tuple.get(member.age);

        System.out.println("username = " + s);
        System.out.println("age = " + i);
    }
}

 

다음과 같이 사용하면 되고 tuple에서 값을 가져오는 방법은 .get에 select 절의 가져오고 싶은 데이터 컬럼 명을 적어주면 된다.

잘 나온다.

 

리포지토리 계층 안에서 튜플을 사용하는 건 괜찮은데

 

서비스 계층이나, 컨트롤러까지 튜플을 가져가면 좋지 않다.

 

리포지토리안에서만 사용해야 설계를 바꿀 때 서비스와 컨트롤러 계층을 변경할 필요가 없다.

 

바깥에 나갈 때는 dto로 변환해서 나가는 것이 좋다.

 

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

@Test
public void findByJPQL(){
    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);
    }
}

 

기존 JPQL에서는 다음과 같이 select 에 아주 긴 new 명령어를 사용해야 dto클래스를 조회할 수 있었다.

Dto의 패키지 명을 다 적어줘야 해서 지저분 하고,

생성자 방식만 지원한다는 불편함이 있다.

 

이러한 문제점을 Querydsl에서 간편하게 해결할 수 있다.

 

  1. 프로퍼티 접근
  2. 필드 직접 접근
  3. 생성자 사용

위의 세 가지 방식을 지원한다.

 

프로퍼티 접근

 

@Test
public void findDtoByQuerydsl(){
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    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);
    }
}

setter를 활용한 방법이다.

Projections.bean을 통해 dto의 생성자를 만들고 setter로 값을 입력해준다.

 

이후 해당 값만 select 하여 결과를 반환해준다.

 

dto에 세터가 없다면 실패한다.

 

필드 직접 접근

@Test
public void findDtoByField(){
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    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);
    }
}

 

bean만 fields로 변경해주면 된다.

 

이 방식은 setter로 접근하는 방식이 아닌 필드에 값을 직접 입력해주는 방식이다.

 

생성자 접근

 

@Test
public void findDtoByConstructor(){
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    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);
    }
}

 

이 방식은 constructor를 입력해주면 되는데

생성자로 접근하는 방식이다 보니 순서가 중요하다.

 

만약 memberdto의 생성자가 age, username 순으로 인자를 입력받는다면,

 

member.age,

member.username

으로 입력받아야 한다.

 

한 가지 방식이 더 있는데 이건 다음 시간에 포스팅 하도록 하겠다.

 

주의할 점

select 절에 적어준 이름과 dto의 이름이 매칭이 되지 않는 경우 null 값이 입력된다.

@Data
public class UserDto {
    private String name;
    private int age;
}

userDto를 다음과 같이 name, age로 필드 명을 설정해 주었다.

@Test
public void findUserDtoByField(){
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    List<UserDto> result = queryFactory
            .select(Projections.fields(UserDto.class,
                    member.username,
                    member.age))
            .from(member)
            .fetch();

}

이후 똑같이 member.username을 출력하려 하면 null 값이 들어가 있는 것을 확인할 수 있다.

 

이유는 username과 dto의 name 즉 이름이 다르기 때문인데 이렇게 dto와 이름이 매칭이 되지 않는경우

 

다음과 같이 as를 사용하여 이름을 매칭시켜줘야 한다.

 

@Test
public void findUserDtoByField(){
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    List<UserDto> result = queryFactory
            .select(Projections.fields(UserDto.class,
                    member.username.as("name"),
                    member.age))
            .from(member)
            .fetch();

}

 

서브쿼리를 사용할 때 별칭 지정 방법

@Test
public void findUserDtoByField(){
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    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를 사용하면 두 번째 인자를 별칭으로 사용할 수 있다.

 

첫 번째 인자에 JPAExpressions를 통해 서브쿼리를 넣어주고 

 

두 번째 인자에 age를 넣어 member.age와 이름을 똑같이 맞춰주었다.

 

다음과 같이 최댓값이 잘 들어가 있는 것을 확인할 수 있다.

 

생성자 접근 방식의 경우 타입을 보고 들어가는 것이기 때문에 이름을 똑같이 맞춰줄 필요는 없다.

 

참고: 김영한 실전! Querydsl