QueryDSL 중급

김신영·2023년 2월 12일
0

JPA

목록 보기
10/14
post-thumbnail

결과 반환

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

  • 코드 예시
    @Test
    void projectionOne() {
        List<String> result = queryFactory
            .select(member.name)
            .from(member)
            .fetch();
    
        assertThat(result).hasSize(4)
            .contains("member1", "member2", "member3", "member4");
    }

프로젝션 대상이 둘 이상인 경우

  • com.querydsl.core.Tuple 혹은 DTO로 조회

Tuple 조회

  • 코드 예시
    @Test
    void projectionTuple() {
        List<Tuple> result = queryFactory
            .select(member.name, member.age)
            .from(member)
            .fetch();
    
        assertThat(result).hasSize(4);
    
        result.forEach(tuple -> {
            System.out.println("tuple.get(member.name) = " + tuple.get(member.name));
            System.out.println("tuple.get(member.age) = " + tuple.get(member.age));
        });
    }

DTO로 조회

순수 JPA에서 DTO 조회

  • JPQL 사용
  • 단, 클래스 명을 패키지 포함해서 상세히 적어야 한다.
  • 오타 발생 시, 컴파일 에러 잡히지 않는다.
  • 생성자 방식만 지원
  • 코드 예시
    @Test
    void projectionDtoByJpql() {
    		// 단, 패키지명까지 상세히 적어야 한다. 
        List<MemberDto> result = em.createQuery(
                "select new com.example.querydsl.controller.dto.MemberDto(m.name, m.age)"
                    + " from Member m", MemberDto.class)
            .getResultList();
    
        assertThat(result).hasSize(4);
    
        result.forEach(System.out::println);
    }

QueryDSL Bean 생성

@Test
void projectionDtoByQueryDsl() {
    List<MemberDto> result = queryFactory
//            .select(Projections.bean(MemberDto.class, member.name, member.age))       // 1. By Property (Setter)
//            .select(Projections.fields(MemberDto.class, member.name, member.age))     // 2. By Field
         .select(Projections.constructor(MemberDto.class, member.name, member.age))  // 3. By Constructor
         .from(member)
         .fetch();
        
     assertThat(result).hasSize(4);
        
     result.forEach(System.out::println);
}
  1. 프로퍼티 접근
    • setter가 필요
    • Projections.bean(MemberDto.class, …)
    • 프로퍼티 이름이 다를 경우, as(alias) 를 통해 별칭 적용한다.
    • 서브쿼리의 별칭의 경우, ExpressionUtils.as(subQuery, alias)
  2. 필드 접근
    • Projections.fields(MemberDto.class, ...)
    • 프로퍼티 이름이 다를 경우, as(alias) 를 통해 별칭 적용한다.
    • 서브쿼리의 별칭의 경우, ExpressionUtils.as(subQuery, alias)
    • 코드 예시
      @Test
      void projectionDtoWithDifferentFieldNames() {
          QMember memberSub = new QMember("memberSub");
      
          List<MemberDto> result = queryFactory
              .select(Projections.constructor(
                      MemberDto.class,
                      member.name.as("name"),
                      ExpressionUtils.as(
                          JPAExpressions
                              .select(memberSub.age.max())
                              .from(memberSub), "age")
                  )
              ).from(member)
              .fetch();
      
          assertThat(result).hasSize(4);
          result.forEach(memberDto ->
              assertThat(memberDto.getAge()).isEqualTo(40)
          );
      }
  3. 생성자 사용
  • Projections.constructor(MemberDto.class, ...)
  • 코드 예시

프로젝션을 통한 DTO 조회

  • 생성자 + @QueryProjection 을 사용
  • DTO 생성자에 @QueryProjectio 어노테이션 적용
  • ./gradlew compileQuerydsl 을 통해 Q-Type의 DTO 클래스 생성
  • 컴파일러로 타입을 체크할 수 있으므로, 안전함
  • 코드 예시
@Data
public class MemberDto {
    private final String name;
    
    private final Integer age;
    
    @QueryProjection
    public MemberDto(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}
    
@Test
void projectionDtoWithQueryProjection() {
    List<MemberDto> result = queryFactory
        .select(new QMemberDto(member.name, member.age))
        .from(member)
        .fetch();
    
    assertThat(result).hasSize(4);
    
    result.forEach(System.out::println);
}

동적 쿼리 (Dynamic Query)

동적 쿼리를 해결하는 두가지 방식

  • BooleanBuilder
  • where(...) 다중 파라미터 사용

BooleanBuilder

  • com.querydsl.core.BooleanBuilder
  • 코드 예시
private List<Member> searchMemberByBooleanBuilder(String name, Integer age) {
    BooleanBuilder builder = new BooleanBuilder();

    if (StringUtils.isNotBlank(name)) {
        builder.and(member.name.eq(name));
    }
        
    if (Objects.nonNull(age)) {
        builder.and(member.age.eq(age));
    }
        
    return queryFactory
        .selectFrom(member)
        .where(builder)
        .fetch();
}

where 다중 파라미터 사용

- `where(...)` 파라미터가 null이면 무시된다.
- 코드 예시
    
    ```java
    private List<Member> searchMemberByWhere(String name, Integer age) {
        return queryFactory
            .selectFrom(member)
            .where(
                Optional.ofNullable(name).map(member.name::eq).orElse(null),
                Optional.ofNullable(age).map(member.age::eq).orElse(null))
            .fetch();
    }
    ```

수정, 삭제 벌크 연산 (Bulk)

  • update(...), delete(…)
  • set(source, target)
  • execute()
  • Bulk 연산의 경우 DB에 직접 쿼리를 수행
  • Bulk 실행 한후, 영속성 컨텍스트 초기화하는 것을 권장

JPQL 배치와 마찬가지로, 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에, 배치 쿼리를 실행한 후 영속성 컨텍스트를 초기화 하는 것이 안전하다.

  • 코드 예시
@Test
void bulkUpdate() {
    long batchCount = queryFactory
        .update(member)
        .set(member.name, member.name.concat("-YOUNG"))
        .where(member.age.lt(28))
        .execute();
        
    assertThat(batchCount).isEqualTo(2);
        
    List<Member> result = queryFactory
        .selectFrom(member)
        .fetch();
        
    result.forEach(member -> {
        if (member.getAge() < 28) {
            assertThat(member.getName()).endsWith("-YOUNG");
        }
    });
}
        
@Test
void bulkUpdateByAddOrMultiply() {
    long batchCount = queryFactory
        .update(member)
        .set(member.age, member.age.add(1))
//            .set(member.age, member.age.multiply(2))
//            .set(member.age, member.age.subtract(10))
//            .set(member.age, member.age.divide(2))
        .execute();
        
     assertThat(batchCount).isEqualTo(4);
        
     List<Member> result = queryFactory
         .selectFrom(member)
         .fetch();
 
     assertThat(result).extracting(Member::getAge)
         .containsOnly(11, 21, 31, 41);
}
        
@Test
void bulkDelete() {
    long batchCount = queryFactory
        .delete(member)
        .where(member.age.gt(18))
        .execute();
        
    assertThat(batchCount).isEqualTo(3);
        
    Long count = queryFactory.select(member.countDistinct()).from(member).fetchOne();
    assertThat(count).isEqualTo(1);
}

SQL Function 호출

  • Expressions.stringTemplate(sqlFunction, ...)
Expressions.stringTemplate("function('replace', {0}, {1}, {2})", member.name, "member", "M")
  • Dialect에 등록된 내용만 호출 가능
  • 대부분 queryDsl에 내장되어 있다.
  • 코드 예시
@Test
void sqlFunctionByExpressionsStringTemplate() {
    List<String> result = queryFactory
        .select(Expressions.stringTemplate("function('replace', {0}, {1}, {2})", member.name,
            "member", "M"))
        .from(member)
        .fetch();
            
    result.forEach(name -> assertThat(name).startsWith("M"));
}
            
@Test
void sqlFunctionByExpressionsStringTemplate2() {
    List<String> result = queryFactory
        .select(member.name)
        .from(member)
        .where(member.name.eq(Expressions.stringTemplate("function('lower', {0})", member.name)))
//            .where(member.name.eq(member.name.lower()))
        .fetch();
            
        result.forEach(name -> assertThat(name).isLowerCase());
    }
profile
Hello velog!

0개의 댓글