Skip to main content Link Search Menu Expand Document (external link)


  • 프로젝션
  • 프로젝션과 결과 반환 (기본)
  • 프로젝션과 결과 반환 (DTO 조회)
    • 순수 JPA 에서 DTO 조회
    • Querydsl 빈 생성(Bean population)
  • 프로젝션과 결과 반환(@QueryProjection)
  • 동적 쿼리
    • BooleanBuilder
    • Where 다중 파라미터 사용
  • 수정, 삭제 벌크 연산
  • SQL function 호출하기


프로젝션

프로젝션(Projection) 은 sql 용어이다. IBM document 에서는 아래와 같이 정의되어 있다.

In relational terminology, projection is defined as taking a vertical subset from the columns of a single table that retains the unique rows. This kind of SELECT statement returns some of the columns and all the rows in a table.

쉽게 column 이라고 봐도 사실 의미는 통한다. 어원은 아무래도 entity(row) 의 일부니까 일부라는 것이 곧 투영(projection) 이라서 projection 이 된게 아닐까 싶다.


프로젝션과 결과 반환 (기본)

프로젝션 대상이 하나

    @Test
    void projectionSingle() {
        List<String> result = queryFactory
                .select(member.username)
                .from(member)
                .fetch();

        result.forEach(System.out::println);
    }
    select
        member0_.username as col_0_0_ 
    from
        member member0_


프로젝션 대상이 둘 이상

com.querydsl.core.Tuple 를 사용한다.

    @Test
    void projectionTuple() {
        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);
        }
    }
    select
        member0_.username as col_0_0_,
        member0_.age as col_1_0_ 
    from
        member member0_

Tuple 은 com.querydsl.core.Tuple 이다. 따라서 이 type 을 infra 계층을 넘어버리면(service 나 application 계층으로 넘어가서까지 사용하면) 특정 기술에 어플리케이션이 의존하게 되어버리므로 유의해서 사용하자. 필요시 일반 DTO 로 변환하여 넘기던가 해야한다.


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

순수 JPA 에서 DTO 조회

    @Test
    void projectionDtoSimple() {
        List<MemberDto> result = em.createQuery(
                        "select new com.example.querydslstudy.MemberDto(m.username, m.age) " +
                                "from Member m", MemberDto.class)
                .getResultList();

        result.forEach(memberDto -> System.out.println(memberDto.toString()));
    }
  • 순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용해야함
  • DTO의 package이름을 다 적어줘야해서 지저분함
  • 생성자 방식만 지원함


Querydsl 빈 생성(Bean population)

프로퍼티 접근, 필드 직접 접근, 생성자 사용 하는 방식이 있다.

    @Test
    void projectionDtoQueryDsl() {
//        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();

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

        result.forEach(memberDto -> System.out.println(memberDto.toString()));
    }


필드명이 다른 DTO 로 조회 결과를 가져오고 싶다면 아래와 같이 사용한다.

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

    public UserDto(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
    @Test
    void fieldNameTest() {
        List<UserDto> result = queryFactory
                .select(Projections.fields(UserDto.class,
                        member.username.as("name"),
                        member.age))
                .from(member)
                .fetch();

        result.forEach(memberDto -> System.out.println(memberDto.toString()));
    }


프로젝션과 결과 반환(@QueryProjection)

package study.querydsl.dto;
  import com.querydsl.core.annotations.QueryProjection;
  import lombok.Data;
  @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;
      }
  }
List<MemberDto> result = queryFactory
          .select(new QMemberDto(member.username, member.age))
          .from(member)
          .fetch();

@QueryProjection 을 사용할 경우 위와 같이 처리해준 뒤 compileQuerydsl 으로 Q class 를 생성해서 사용해야한다.

이 방식은 굳이 Q class 를 만들어줘야하고, DTO 가 POJO 하지 못하고 특정 기술에 의존하게 되므로 썩 좋지는 않은 것 같다.


동적 쿼리

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();
    }
    select
        member0_.member_id as member_i1_1_,
        member0_.age as age2_1_,
        member0_.team_id as team_id4_1_,
        member0_.username as username3_1_ 
    from
        member member0_ 
    where
        member0_.username=? 
        and member0_.age=?

위와 같이 사용하면 된다. 아래는 내가 이메탈 프로젝트시 사용했던 코드 일부이다.

    @Override
    public Page<Auction> findAllByAuctionTypeAndMetalAndIsDisplayed(AuctionType auctionType, Metal targetMetal, YesNoType isDisplayed, Pageable pageable) {
        BooleanBuilder builder = new BooleanBuilder();

        JPQLQuery<Auction> query = queryFactory.selectFrom(auction)
                .innerJoin(auction.hostUser, user)
                .fetchJoin()
                .innerJoin(auction.auctionItem.metal, metal)
                .fetchJoin()
                .innerJoin(auction.auctionItem.metalOption, metalOption)
                .fetchJoin();

        if (auctionType != null) {
            builder.and(auction.auctionType.eq(auctionType));
        }

        if (targetMetal != null) {
            builder.and(auction.auctionItem.metal.eq(targetMetal));
        }

        if (isDisplayed != null) {
            builder.and(auction.isDisplayed.eq(isDisplayed));
        }

        query = query.where(builder);
        long totalCount = query.fetchCount();
        List<Auction> results = getQuerydsl().applyPagination(pageable, query).fetch();

        return new PageImpl<>(results, pageable, totalCount);
    }


Where 다중 파라미터 사용

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

CH03 기본문법에서 이미 알아봤지만 where 절이 아래와 같이 구성되어 있기 때문에 위와 같은 처리가 가능하다.

/**
 * Add the given filter conditions
 *
 * <p>Skips null arguments</p>
 *
 * @param o filter conditions to be added
 * @return the current object
 */
public Q where(Predicate... o) {
        return queryMixin.where(o);
        }

null 이 반환이 되어도 아무 문제가 없는 것이다. 위와 같은 방식으로 하면 BooleanBuilder 를 사용했을 때보다 가독성 및 코드 재활용성이 좋아진다.

  • BooleanExpression 를 반환하는 개별 private 함수를 다른 곳에서 재활용 할 수 있고,
  • 동적쿼리를 수행하는 메인 로직에서 쿼리를 먼저 읽게 되기 때문이다.(BooleanExpression 를 사용하는 방식에선 쿼리를 가장 나중에 보게 된다는 것과 비교해보면 이해하기 쉽다.)

아래와 같이 조합도 가능하다. 조합을 하면 좋은 장점이 아래 예시처럼 함수명이 allEq 이 아니라 isAvailable() 과 같이 도메인 용어나 도메인 지식이 들어간 의미 있는 함수명을 써서 코드의 가독성을 높힐 수 있기 때문이다.

 private BooleanExpression allEq(String usernameCond, Integer ageCond) {
        return usernameEq(usernameCond).and(ageEq(ageCond));
 }


수정, 삭제 벌크 연산

    @Test
    void batchUpdateTest() {
        long count = queryFactory
                .update(member)
                .set(member.username, "비회원")
                .where(member.age.lt(28)).execute();

        System.out.println("count : " + count);
    }
    update
        member 
    set
        username=? 
    where
        age<?

2023-03-31 16:34:03.221 TRACE 32760 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [비회원]
2023-03-31 16:34:03.224 TRACE 32760 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [INTEGER] - [28]


    @Test
    void batchDeleteTest() {
        long count = queryFactory
                .delete(member)
                .where(member.age.gt(18))
                .execute();

        System.out.println("count : " + count);
    }
    delete 
    from
        member 
    where
        age>?
2023-03-31 16:37:44.503 TRACE 32784 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [INTEGER] - [18]

주의 할 점은 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에 배치 쿼리를 실행하고 나면 영속성 컨텍스트를 초기화 해줘야 안전하다. 즉, DML 실행 전 혹은 후에 entityManager.flush(), entityManager.clear() 처리를 해줘야 한다는 것이다.

그렇게 해주지 않으면 하나의 트랜잭션 내에서 해당 DML 이 실행되고 나서 DB와 1차 캐시 간에 데이터 불일치가 발생할 수 있다. 이 불일치는 후에 어떠한 처리를 하느냐에 따라 의도되지 않은 결과를 가져올 수 있다.


SQL function 호출하기

SQL function은 JPA와 같이 Dialect에 등록된 내용만 호출할 수 있다.

String result = queryFactory
            .select(Expressions.stringTemplate("function('replace', {0}, {1},
            {2})", member.username, "member", "M"))
            .from(member)
            .fetchFirst();

사용하는 database 의 Dialect 를 queryDsl 내에서 호출해서 사용이 가능하다는 것을 인지하고 있는 정도만 해도 충분하다. 쓸 일이 개인적으로 없었고, 앞으로도 굳이 있을까 싶다. 데이터 조작 자체는 어플리케이션 내에서 처리하는 것을 지향하는 것이 좋을 것 같다.