본문 바로가기
관리자

Programming-[Backend]/JPA

[Querydsl] 4. 중급 문법 - Projections, BooleanBuilder

728x90
반응형

 

1. 특정 DTO로 원하는 필드값만 가져오기

 

 

복습겸 JPQL로 원하는 필드값만 가져오는 것을 연습한다. MemberDto를 만들고 member.username, member.age 값을 가져와본다.

 

 

순수 JPA에서 JPQL로 Dto 조회

select 절에 new Operator를 사용해서 MemberDto를 가져올 수 있도록 작성한다. 특이한 점은 해당 Dto가 있는 패키지 위치를 전부 작성해야된다는 점이고, 타입은 MemberDto.class로 가져오면서도 from절은 Member 엔티티로 지정하고, MemberDto의 필드값도 이 엔티티 기반으로 m.username, m.age 로 작성해준다는 점이다.

 

 

MemberDto.class

생성자가 반드시 있어야 한다.

 

1
2
3
4
5
6
7
@Data
@AllArgsConstructor
public class MemberDto {
 
  private String username;
  private Integer age;
}
cs

 

테스트코드

1
2
3
4
5
6
7
8
9
10
11
@Test
  void memberDToJPQL() throws Exception {
 
    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);
    }
 
  }
cs

 

 

 

querydsl의 Projections 활용

 

querydsl에서의 Projections를 활용하면 더 편하게 사용할 수 있다. 다만, JPA가 dto를 만들어줄 때 기본생성자가 필요해서 MemberDto에 @NoArgsConstructor를 추가한다.

 

Projections로 조회 시, 어떤 것을 기반으로 하여 조회할지 설정해줄 수 있다.

 

Projections.bean 은 setter 기반이라서 MemberDto에 반드시 Setter가 있어야 한다. 아래 예제 코드에서는 @Data 어노테이션에 @Setter가 있으므로 정상적으로 동작한다. 그외 fields는 필드 적용, constructor는 생성자를 이용하는 방식이다.

 

Projections.fields 방식은 dto에서 정의한 필드의 이름과 select절에서의 각 필드명이 일치해야 한다. 만약 일치할 수 없는 상황이라면 .as("별칭")을 주면 된다.

 

Projections.constructor 방식의 경우 조회를 원하는 username, age를 포함한 생성자가 반드시 있어야 한다. 그리고 dto에서 생성자의 파라미터들의 타입과 순서가 select 절에서의 타입과 순서와 일치해야한다. 또한 compile 전에 에러를 잡아주지 못한다는 단점이 있다.

 

 

MemberDto.class

 

1
2
3
4
5
6
7
8
9
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MemberDto {
 
  private String username;
  private Integer age;
}
 
cs

 

 

Setter 기반

 

1
2
3
4
5
6
7
8
9
10
11
12
@Test
  void findDtoBySetter() throws Exception {
    List<MemberDto> result = jpaQueryFactory
            .select(Projections.bean(MemberDto.class,
                    member.username,
                    member.age))
            .from(member)
            .fetch();
    for (MemberDto memberDto : result) {
      System.out.println("memberDto = " + memberDto);
    }
  }
cs

 

Field 직접 활용

 

1
2
3
4
5
6
7
8
9
10
11
12
@Test
  void findDtoByField() throws Exception {
    List<MemberDto> result = jpaQueryFactory
            .select(Projections.fields(MemberDto.class,
                    member.username,
                    member.age))
            .from(member)
            .fetch();
    for (MemberDto memberDto : result) {
      System.out.println("memberDto = " + memberDto);
    }
  }
cs

 

Constructor 사용

 

1
2
3
4
5
6
7
8
9
10
11
12
@Test
  void findDtoByConstructor() throws Exception {
    List<MemberDto> result = jpaQueryFactory
            .select(Projections.constructor(MemberDto.class,
                    member.username,
                    member.age))
            .from(member)
            .fetch();
    for (MemberDto memberDto : result) {
      System.out.println("memberDto = " + memberDto);
    }
  }
cs

 

 

alias 설정하기 : .as(), ExpressionUtils.as()

 

만약 UserDto와 같은 class가 있어서 username 이 아닌 name 이라는 필드값으로 조회해야되는 경우, 위에서 언급한 바와 같이 alias를 설정해주면 된다.

 

그리고, 아래 코드에서 확인할 수 있는 것처럼 서브 쿼리에 alias를 적용해야한다면 ExpressionUtils.as() 문법을 사용하면 된다.

 

UserDto.class

1
2
3
4
5
6
7
8
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDto {
 
  private String name;
  private Integer age;
}
cs

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
  void findUserDto() throws Exception {
    QMember memberSub = new QMember("memberSub");
    List<Tuple> result = jpaQueryFactory
            .select(Projections.fields(UserDto.class),
                    member.username.as("name"),
                    ExpressionUtils.as(
                            JPAExpressions
                                    .select(memberSub.age.max())
                                    .from(memberSub), "age")
            )
            .from(member)
            .fetch();
 
    System.out.println("result = " + result);
  }
cs

 

 

 


 

2. @QueryProjection으로 Dto를 Q객체로 만들기

 

 

조회를 원하는 타입의 Dto에 @QueryProjection 어노테이션을 적용할 수도 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@NoArgsConstructor
public class MemberDto {
 
  private String username;
  private Integer age;
 
  @QueryProjection
  public MemberDto(String username, Integer age) {
    this.username = username;
    this.age = age;
  }
 
}
cs

 

compileQuerydsl을 실행하면 MemberDto에 대해서도 Q 객체가 생성되는 것을 확인할 수 있다.

 

그리고 생성된 Q객체를 new 키워드와 함께 사용하여 select문에 넣어주면 된다.

 

1
2
3
4
5
6
7
8
9
10
11
@Test
  public void findDtoByQueryProjection() {
    List<MemberDto> result = jpaQueryFactory
            .select(new QMemberDto(member.username, member.age))
            .from(member)
            .fetch();
 
    for (MemberDto memberDto : result) {
      System.out.println("memberDto = " + memberDto);
    }
  }
cs

 

 

이 @QueryProjection을 적용하면 constructor 방식에 비해서 compile 전에 바로 에러를 잡아낼 수 있는 장점이 있다. 다만 Q 파일을 미리 생성해야되고 Dto 자체가 @QueryProjection 어노테이션에 의해 querydsl에 의존성을 갖게되는 큰 문제가 있다.

 

 


 

3. 동적 쿼리 : BooleanBuilder

 

 

동적 쿼리는 검색 등에서 사용될 수 있도록 쿼리문에 작성되는 조건들을 nullable 하게 넣어줄 수 있는 기능이다. 우선 BooleanBuilder를 적용하는데, 사용은 아래와 같이 하면 된다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
  public void dynamicQuery_BooleanBuilder() {
    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 jpaQueryFactory
            .selectFrom(member)
            .where(builder)
            .fetch();
  }
cs

 

 

조건문만 봐도, 파라미터로 받아오는 username이나 age가 null 일때와 아닐때 모두 쿼리문에 동적으로 적용되는 것을 알 수 있다. 실무에서는 if문을 안쓰고 삼항연산자를 써서 ageCond == null ? null : builder.and(member.age.eq(ageCond) 로 사용하기도 했다.

 

 


 

4. 동적 쿼리 : Where문 다중 파라미터 사용

 

BooleanBuilder 대비 더 깔끔하게 코드를 짤 수 있는 방법이다. 조건 자체를 메서드로 한번 더 뽑아서 BooleanExpression 타입으로 반환하게 만든다. 이럴 경우, 만약 usernameEq가 null 이면 where절에 null이 들어가고, where절에서의 null은 무시되므로 동적 쿼리가 가능해진다. 메서드명이 명확하므로 앞서 BooleanBuilder로 조건문을 일일이 다 살피던 방식과 다르게 굳이 조건문을 자세히 살펴보지 않아도 바로 쿼리문을 이해할 수 있다.

 

또 다른 장점은 allEq와 같이 BooleanExpression을 조합해서 사용할 수 있다는 점이다. 다만 .and와 같이 붙여서 사용할 때는 if 문 등으로 분기하여 null 체크를 반드시 해줘야한다.

 

끝으로 이렇게 BooleanExpression을 만들어놓으면 재사용성이 높다는 장점이 있다. 만든 검색조건문을 member 엔티티를 조회하는 곳에서 뿐만 아니라 다른 형태의 결과를 조회할 때도 사용할 수 있다. 보통 이러한 BooleanExpression 자체를 파라미터만 넣으면 바로 사용할 수 있도록 공통화하는 구조도 많이 사용한다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Test
  public 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 jpaQueryFactory
            .selectFrom(member)
//            .where(usernameEq(usernameCond), ageEq(ageCond))
            .where(allEq(usernameCond, 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));
  }
cs

 

 

 

 


 

참조

 

1. 인프런_실전! Querydsl_김영한 님 강의

https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84/dashboard

728x90
반응형