[Querydsl] 중급문법 학습하기

이성혁·2022년 1월 20일
1

Querydsl

목록 보기
2/2
post-thumbnail
post-custom-banner

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

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

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

  • 순수 JPA에서 DTO 조회 코드
@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은 다음과 같은 방법을 지원한다.

Querydsl 빈 생성(Bean population)

  • setter - 프로퍼티 접근
@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);
    }
}
  • field - 필드 직접 접근

@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);
    }
}
  • constructor - 생성자 접근
@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") : 필드에 별칭 적용

프로젝션과 결과 반환 @QueryProjection

  • 생성자 + @QueryProjection
@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);
    }
}

QueryProjectionDTO 생성자를 Q-type으로 미리 생성한 뒤 사용한다. 가장 큰 장점은 생성자를 이용하여 생성하기 때문에 컴파일 단계에서 에러를 확인할 수 있다. 그리고 입력 필드를 IDE를 통해서 확인할 수 있으므로 개발할 때 많은 편리함을 가져올 수 있다. 하지만 순수한 DTO가 아니라 Querydsl에 의존성을 가지기 때문에 도입할 때 주의를 요구한다.

동적 쿼리 BooleanBuilder 사용

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

동적 쿼리 Where 다중 파라미터 사용

@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 배치와 마찬가지로, 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에 배치 쿼리를 실행하고 나면 영속성 컨텍스트를 초기화 하는 것이 안전하다.

profile
항상 배우는 자세로 🪴
post-custom-banner

0개의 댓글