자바생
article thumbnail
728x90

프로젝션?

  • 데이터베이스에서 프로젝션은 조건에 맞는 릴레이션의 속성을 추출한다는 의미이다
  • 따라서 우리는 원하는 속성을 추출하여 얻을 수 있다
  • 만약에 프로젝션 대상이 한개라면 타입을 "명확"하게 정의할 수 있다
  • 하지만 둘 이상이라면 "튜플"이나 "DTO"를 사용해야한다

 

순수 JPA에서의 DTO 조회

 

List<MemberDto> result = em.createQuery("select new study.querydsl.dto.MemberDto(m.username, m.age) " +
                                           "from Member m", MemberDto.class)
                                   .getResultList();
  • 순수 JPA에서 DTO를 조회하기 위해서는 new 명령어를 이용하여 패키지 구조를 다 적어줘야함
  • 또한, 생성자 방식만 지원함

위와 같은 이유로(내 생각엔 패키지 명 적는게 제일 큼) 순수 JPA에서의 DTO 조회는

코드를 지저분할 뿐만 아니라 귀찮다고 생각한다

이에 querydsl은 이러한 단점을 보완하기 위해 DTO를 반환할 때 3가지 방법을 지원한다

 

Querydsl에서의 DTO 조회

  • querydsl 빈 생성(Bean population)이라 함
  • 프로퍼티 접근, 필드 접근, 생성자 사용, 총 3가지 방법을 지원한다

 

MemberDto.class

@Data
@NoArgsConstructor
public class MemberDto {

    private String username;
    private int age;

    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

 

프로퍼티 접근

 

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

 

필드 접근

 

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

만약 엔티티의 필드명과 변환할 DTO의 필드명이 다르면 어떡할까?

UserDto.class

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {

    private String name;
    private int age;
}
@Test
@DisplayName("엔티티의 필드명과 변환할 DTO 필드명이 다를 경우 인식하지 못함")
void findUserDto() throws Exception {
    List<UserDto> result = queryFactory
            .select(Projections.fields(UserDto.class,
                    member.username,
                    member.age))
            .from(member)
            .fetch();

    for (UserDto userDto : result) {
        System.out.println("userDto = " + userDto);
    }
}

Member에서 name을 나타내는 필드명은 username이다

이처럼 UserDto의 필드명은 name, Member의 필드명이 username일 때, DTO를 조회하게 되면

name 필드가 제대로 인식되지 못하는 것을 알 수 있다

@Test
void findUserDto() throws Exception {
    QMember memberSub = new QMember("memberSub");

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

    for (UserDto userDto : result) {
        System.out.println("userDto = " + userDto);
    }
}

"as" 메서드를 사용해서 필드에 "별칭"을 적용할 수 있다

ExpressionUtils는 무엇인가??

as를 사용하면 필드에 별칭을 적용할 수 있지만

ExpressionUtils.as를 사용하면 "필드"나, "서브 쿼리"에 별칭을 적용할 수 있다

 

위 코드는 age의 max 값을 UserDto의 age에 적용하고 있다

 

생성자 사용

@Test
@DisplayName("querydsl DTO 반환 by 생성자")
void findDtoByConstructor() throws Exception {
    List<MemberDto> result = queryFactory
            .select(Projections.constructor(MemberDto.class,
                    member.username,
                    member.age))
            .from(member)
            .fetch();

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

    for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
    }

    for (UserDto userDto : result2) {
        System.out.println("userDto = " + userDto);
    }
}

프로퍼티나 필드에 접근할 때와 다르게 생성자 사용 방법에서는 엔티티의 필드명과 변환할 DTO 필드명이 달라도 상관없다

어떻게 보면 생성자에 접근하기 때문에 달라도 상관없는 것이 당연하다 생각한다

 

@QueryProjection

위에서 생정자 방법을 사용하기 위해서는 select에서 Projections.constructor를 이용했다

 

하지만 DTO 클래스에서 생성자에 @QueryProjection 을 사용하면 간편하게 DTO를 반환할 수 있다

 

MemberDto.class

@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을 사용
  • gradle -> other -> compileQuerydsl ( ./gradlew compileQuerydsl)
  • QMemberDto가 생성됐는지 확인
@Test
@DisplayName("@QueryProjection 사용")
void findDtoByQueryProjection() throws Exception {
    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에 @QueryProjection을 사용하면서 querydsl에 의존하게 된다
  • 또한, Q 클래스를 생성해야하므로 단점이 있다

 

동적 쿼리

동적 쿼리를 해결하는 방식

  • BooleanBuilder
  • Where 다중 파라미터

 

BooleanBuilder

@Test
@DisplayName("BooleanBuilder 사용")
void booleanBuilder() throws Exception {
    String usernameParam = "member1";
    Integer ageParam = 10;

    List<Member> result = searchMember1(usernameParam, ageParam);

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

 

 

searchMember1 파라미터 null일 경우

List<Member> result = searchMember1(null, ageParam);

usrename 파라미터가 null이 된다면 어떤 쿼리가 나갈지 확인해보자

그러면 age 조건만 where 절에 들어간 것을 알 수 있다

 

where 다중 파라미터 사용

@Test
@DisplayName("where 다중 파라미터 사용")
void whereParam() throws Exception {
    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)) //null이 나오면 해당조건을 무시
            .fetch();
}

private BooleanExpression usernameEq(String usernameCond) {
    if (usernameCond == null) {
        return null;
    }

    return member.username.eq(usernameCond);

}
private BooleanExpression ageEq(Integer ageCond) {
    return ageCond != null ? member.age.eq(ageCond) : null;
}

  • where 조건에 null 값은 무시된다
  • 메서드를 다른 쿼리에서도 재활용 할 수 있다

 

serachMember2의 메서드에서 username이 null일 경우 age 조건만 나가는 것을 알 수 있다

 

    @Test
    void whereParam() throws Exception {
        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(allEq(usernameCond, ageCond))
                .fetch();
    }
    
    private Predicate allEq(String usernameCond, Integer ageCond) {
        return usernameEq(usernameCond).and(ageEq(ageCond));
    }
  • allEq와 같이 조합할 수 있다

 

 

 

 

728x90
profile

자바생

@자바생

틀린 부분이 있다면 댓글 부탁드립니다~😀

검색 태그