Querydsl-중급 및 실무 활용

개발하는 도비·2023년 5월 10일

JPA

목록 보기
13/13
post-thumbnail

중급 문법

프로젝션과 결과 반환

  • 프로젝션: select 대상 지정

대상 하나

  • 예제
@Test
public void simpleProjection(){
    List<String> result = queryFactory
            .select(member.username)
            .from(member)
            .fetch();
    System.out.println("result = " + result);
}

튜플 조회

  • 프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회 권장
    • 튜플도 Querydsl에 종속적이기 때문에 repository 밖으로 나갈때는 DTO 궈낮ㅇ
  • 예제
@Test
public 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

  • 특징
    • new 명령어를 사용
    • DTO package를 다 적어야함
    • 생성자 방식만 지원
  • 예제
@Test
public 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);
        }
    }

Querydsl 빈 생성

  • Querydsl 지원 방식

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

    @Test
    public 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);
        }
    }
  • 필드 직접 접근

    @Test
    public 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);
        }
    }
  • 생성자 사용

    • field -> constructor로 바꾸면 됨.
    @Test
    public 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);
        }
    }
  • 추가 : 별칭이 다를 때

    @Test
    public void findUserDto (){
    
        List<UserDto> result = queryFactory
                .select(Projections.fields(UserDto.class,
                        member.username.as("name"),
                        member.age))
                .from(member)
                .fetch();
        for (UserDto userDto : result) {
            System.out.println("memberDto = " + userDto);
        }
    }

프로젝션과 결과 반환 - @QueryProjection

  • 생성자애 @QueryProjection 추가
    • ./gradlew compileQuerydsl
    • QMemberDto 생성 확인
    @Data
    public class MemberDto {
        private String username;
        private int age;
        public MemberDto() {
        }
        @QueryProjection
        public MemberDto(String username, int age) {
            this.username = username;
            this.age = age;
        }
    }
  • 생성자 + @QueryProjection 예제
    @Test
    public 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);
        }
    }
  • 기존 생성자 사용 vs 생성자 + @QueryProjection
    • 기존 생성자 사용 : 런타임에서 에러 확인 가능
    • 생성자 + @QueryProjection : 컴파일에서 에러 확인 가능
  • distinct는 JPQL의 distinct와 같음.

동적 쿼리

BooleanBuilder 사용

  • 예제
@Test
public void 동적쿼리_BooleanBuilder() throws Exception {
    String usernameParam = "member1";
    Integer ageParam = 10;
    List<Member> result = searchMember1(usernameParam, ageParam);
    Assertions.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 다중 파라미터 사용

  • 특징
    • where 조건에 null 값은 무시.
    • 메서드를 다른 쿼리에서도 재활용.
    • 쿼리 자체의 가독성이 높아진다.
  • 예제
@Test
public void 동적쿼리_WhereParam() throws Exception { String usernameParam = "member1";
    Integer ageParam = 10;
    List<Member> result = searchMember2(usernameParam, ageParam);
    Assertions.assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember2(String usernameCond, Integer ageCond) {
    return queryFactory
            .selectFrom(member)
            .where(usernameEq(usernameCond), ageEq(ageCond))
            .fetch();
}
private BooleanExpression usernameEq(String usernameCond) {
    return usernameCond != null ? member.username.eq(usernameCond) : null;
}
private BooleanExpression ageEq(Integer ageCond) {
    return ageCond != null ? member.age.eq(ageCond) : null;
}
  • 조합이 가능.
    • 각각의 메서드를 조합 가능
    private BooleanExpression allEq(String usernameCond, Integer ageCond) {
        return usernameEq(usernameCond).and(ageEq(ageCond));

수정, 삭제 벌크 연산

  • 영속성 컨텍스트에 있는 entity를 무시하고 실행되기 때문에 배치 쿼리를 실행하고 나면 영속성 컨텍스트를 초기화 하는 것이 안전.
  • DB에 바로 바꾸기 때문에 DB와 영속성 컨텍스트와 같이 달라지기 때문.
  • DB에서 select 해도 영속성 컨텍스트가 우선권을 가짐 -> DB에서 가져온 값을 무시해버림.
  • 벌크 이후 em.flush(), em.clear() 해줄 것
  • 쿼리 한번으로 대량 데이터 수정
    @Test
    public void bulkUpdate(){
          long count = queryFactory
                  .update(member)
                  .set(member.username, "비회원")
                  .where(member.age.lt(28))
                  .execute();
      }
  • 기존 숫자에 1 더하기
    @Test
    public void bulkAdd(){
      long count = queryFactory
              .update(member)
              .set(member.age, member.age.add(1))
              .execute();
    }
  • 곱하기: multiply(x)
    @Test
    public void bulkMultiply(){
      long count = queryFactory
              .update(member)
              .set(member.age, member.age.multiply(2))
              .execute();
    }
  • 쿼리 한번으로 대량 데이터 삭제
    @Test
    public void bulkDelete(){
      long count = queryFactory
              .delete(member)
              .where(member.age.gt(18))
              .execute();
    }

SQL function 호출하기

  • SQL function은 JPA와 같이 Dialect에 등록된 내용만 호출
  • member M으로 변경하는 replace 함수 사용
    @Test
    public void sqlFunction(){
      String result = queryFactory
              .select(Expressions.stringTemplate(
                      "function('replace', {0}, {1}, {2})",
                      member.username, "member", "M"))
              .from(member)
              .fetchFirst();
    }
  • 소문자로 변경해서 비교
    @Test
    public void sqlFunction2(){
      List<String> result = queryFactory
              .select(member.username)
              .from(member)
              .where(member.username.eq(Expressions.stringTemplate("function('lower', {0})",
                      member.username)))
              .fetch();
    }
    • lower 같은 ansi 표준 함수들은 querydsl이 상당부분 내장
    @Test
    public void sqlFunction2(){
      List<String> result = queryFactory
              .select(member.username)
              .from(member)
              .where(member.username.eq(member.username.lower()))
              .fetch();
    }

실무 활용

동적 쿼리와 성능 최적화 조회 - Builder 사용

  • 동적 쿼리 + 성능 최적화 + DTO 조회
  • 예제
    public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition) {
      BooleanBuilder builder = new BooleanBuilder();
      if (hasText(condition.getUsername())) {
          builder.and(member.username.eq(condition.getUsername()));
      }
      if (hasText(condition.getTeamName())) {
          builder.and(team.name.eq(condition.getTeamName()));
      }
      if (condition.getAgeGoe() != null) {
          builder.and(member.age.goe(condition.getAgeGoe()));
      }
      if (condition.getAgeLoe() != null) {
          builder.and(member.age.loe(condition.getAgeLoe()));
      }
      return queryFactory
              .select(new QMemberTeamDto(
                      member.id,
                      member.username,
                      member.age,
                      team.id,
                      team.name))
              .from(member)
              .leftJoin(member.team, team)
              .where(builder)
              .fetch();
      }

동적 쿼리와 성능 최적화 조회 - Where절 파라미터 사용

  • 예제
    public List<MemberTeamDto> search(MemberSearchCondition condition) {
          return queryFactory
                  .select(new QMemberTeamDto(
                          member.id,
                          member.username,
                          member.age,
                          team.id,
                          team.name))
                  .from(member)
                  .leftJoin(member.team, team)
                  .where(usernameEq(condition.getUsername()),
                          teamNameEq(condition.getTeamName()),
                          ageGoe(condition.getAgeGoe()),
                          ageLoe(condition.getAgeLoe()))
                  .fetch();
      }
      private BooleanExpression usernameEq(String username) {
          return isEmpty(username) ? null : member.username.eq(username);
      }
      private BooleanExpression teamNameEq(String teamName) {
          return isEmpty(teamName) ? null : team.name.eq(teamName);
      }
      private BooleanExpression ageGoe(Integer ageGoe) {
          return ageGoe == null ? null : member.age.goe(ageGoe);
      }
      private BooleanExpression ageLoe(Integer ageLoe) {
          return ageLoe == null ? null : member.age.loe(ageLoe);
      }

사용자 정의 리포지토리

  • 사용법
    • 사용자 정의 인터페이스 작성
    • 사용자 정의 인터페이스 구현
    • 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속

스프링 데이터 페이징 활용

count 분리

  • count 분리 x 예제
    @Override
     public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
        QueryResults<MemberTeamDto> results = queryFactory
                .select(new QMemberTeamDto(
                        member.id,
                        member.username,
                        member.age,
                        team.id,
                        team.name))
                .from(member)
                .leftJoin(member.team, team)
                .where(usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();
        List<MemberTeamDto> content = results.getResults();
        long total = results.getTotal();
        return new PageImpl<>(content, pageable, total);
    }
  • count 분리 예제
    • 전체 카운트를 조회 최적화 가능할 떄 아래와 같이 분리 가능
    • 실제 content와 별개로 total count는 쉽게 구할 수 있는 경우가 있음. -> ex) join 로직이 필요 없는 경우
    • 혹은 count를 먼저하고 없을 경우 select를 안하는 등의 선택지도 존재.
     @Override
      public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
          List<MemberTeamDto> content = queryFactory
                  .select(new QMemberTeamDto(
                          member.id,
                          member.username,
                          member.age,
                          team.id,
                          team.name))
                  .from(member)
                  .leftJoin(member.team, team)
                  .where(usernameEq(condition.getUsername()),
                          teamNameEq(condition.getTeamName()),
                          ageGoe(condition.getAgeGoe()),
                          ageLoe(condition.getAgeLoe()))
                  .offset(pageable.getOffset())
                  .limit(pageable.getPageSize())
                  .fetch();
          long total = queryFactory
                  .select(member)
                  .from(member)
                  .leftJoin(member.team, team)
                  .where(usernameEq(condition.getUsername()),
                          teamNameEq(condition.getTeamName()),
                          ageGoe(condition.getAgeGoe()),
                          ageLoe(condition.getAgeLoe()))
                  .fetchCount();
          return new PageImpl<>(content, pageable, total);
      }

CountQuery 최적화

  • 스프링 데이터 라이브러리가 제공

  • count 쿼리가 생략 가능한 경우 생략해서 처리

    • 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때
    • 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함)
  • 예제(count 분리 예제 변경)

    • fetchCount()를 해야햐만 쿼리가 날아감
    • PageableExecutionUtils.getPage()의 경우 위의 조건의 경우 fetchCount() 실행
      @Override
      public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
          List<MemberTeamDto> content = queryFactory
                  .select(new QMemberTeamDto(
                          member.id,
                          member.username,
                          member.age,
                          team.id,
                          team.name))
                  .from(member)
                  .leftJoin(member.team, team)
                  .where(usernameEq(condition.getUsername()),
                          teamNameEq(condition.getTeamName()),
                          ageGoe(condition.getAgeGoe()),
                          ageLoe(condition.getAgeLoe()))
                  .offset(pageable.getOffset())
                  .limit(pageable.getPageSize())
                  .fetch();
          JPAQuery<Member> countQuery = queryFactory
                  .select(member)
                  .from(member)
                  .leftJoin(member.team, team)
                  .where(usernameEq(condition.getUsername()),
                          teamNameEq(condition.getTeamName()),
                          ageGoe(condition.getAgeGoe()),
                          ageLoe(condition.getAgeLoe()));
    
          //        return new PageImpl<>(content, pageable, total);
          return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);
    
      }
    

참조

  • 인프런 : 실전! Querydsl
  • 링크
profile
도비의 양말을 찾아서

0개의 댓글