[Query DSL] 중급 문법

홍정완·2022년 8월 13일
0

JPA

목록 보기
26/38
post-thumbnail

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


  • select 절에서 가져올 것을 지정해서 가져오는 것을 프로젝션이라 한다.

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

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

    for (String s : result) {
        System.out.println("s = " + s);
    }
}
  • 프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있다.(List<String>)
  • 프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회한다.



프로젝션 대상이 둘 이상일 때 - 튜플 조회

com.querydsl.core.Tuple

@Test
public void tupleProjection() throws Exception {
    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("tuple ="+ tuple); // tuple = [member1, 10]
        System.out.println("age = " + age);  //10
        System.out.println("username = " + username); //member1
    }
}
  • Tuple은 Repository 계층 안에서 쓰는 정도는 괜찮지만 그 밖의 계층에서는 DTO로 변환해서 쓰는 것이 좋다.
    • Tuple도 결국 Querydsl에 종속적이기 때문이다.



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


  • 위에서 select 절에서 대상을 지정(프로젝션) 해서 가져오고 싶을 때 그 대상이 둘 이상이면 Tuple로 가져왔었다.
  • 하지만, 그 외 계층으로 가져갈 때는 DTO로 가져가는 게 좋다. 우선 순수 JPA를 이용해서 DTO로 조회하는 코드를 작성해 보자

순수 JPA에서 DTO를 조회

@Test
public void findDtoJPQL() 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);
    }
}
  • 순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용해야 한다.
  • DTO의 full package path를 모두 적어줘야 한다.
  • 생성자 방식만 지원한다.



Querydsl 빈 생성(Bean pupulation)

  • 결과를 DTO로 반환할 때 사용하며 3가지 방법이 존재한다.
    • 프로퍼티 접근
    • 필드 직접 접근
    • 생성자 사용

프로퍼티 접근 - Setter

@Test
public void findDtoBySetter() throws Exception {
    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);
    }
}
  • Projections.bean(주입 대상 클래스, 프로퍼티 1, 프로퍼티 2...) 방식으로 프로퍼티에 값을 주입한다.

필드 직접 접근

@Test
public void findDtoByField() throws Exception {
    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);
    }
}
  • Getter, Setter가 없어도 필드에 바로 값을 주입한다.
  • Projections.fields(주입 대상 클래스, 필드 1, 필드 2...)

필드 직접 접근 - 별칭이 다를 때

@Test
public void findUserDtoByField() throws Exception {
    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 memberDto : result) {
        System.out.println("memberDto = " + memberDto);
    }
}
  • 필드나 프로퍼티명 접근 생성 방식에서 이름이 다를 때 해결 방안을 as('name')을 이용해서 해결한다.
  • ExpressionUtils.as(source, alias) : 필드나 서브 쿼리에 별칭 적용
  • username.as("memberName") : 필드에 별칭 적용

생성자 사용

@Test
public void findDtoByConstructor() throws Exception {
    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);
    }
}



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


  • 프로젝션 결과를 반환할 DTO에 @QueryProjection을 생성자에 붙혀준 뒤 gradle → tasks → other → compileQuerydsl을 통해 DTO도 Q 타입으로 생성해 준다.

생성자 + @QueryProjection

@Data
@NoArgsConstructor
public class MemberDto {
    private String username;
    private int age;

    @QueryProjection
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}
  • ./gradlew compileQuerydsl
  • QMemberDto 생성 확인

@QueryProjection 활용

@Test
public void findDtoByQueryProjection() throws Exception{
    List<MemberDto> result = queryFactory
            .select(new QMemberDto(member.username, member.age))
            .from(member)
            .fetch();

    for (MemberDto memberDto : result) {
        System.out.println("memberDto = " + memberDto);
    }
}
  • 이 방법은 컴파일러로 타입을 체크할 수 있기에 가장 안전한 방법이다.
    하지만 DTO에 QueryDSL 어노테이션을 유지해야 하는 점과 DTO까지 Q 파일을 생성해야 하는 단점이 있다.

distinct 사용

@Test
public void distinct_test() throws Exception{
    List<String> result = queryFactory
            .select(member.username).distinct()
            .from(member)
            .fetch();
}



동적 쿼리 - BooleanBuilder 사용


예전 순수 JPA에서 동적 쿼리를 해결하는 방법으로 3가지가 있었다.

  • 문자열 조합 → 조건에 따라 문자열을 결합하면서 query 문을 만들고 parameter를 세팅해 주는 방법
  • JPA Criteria → JPA 표준 스펙에서 제공하는 기능.
  • queryDSL → 오픈소스를 통해 제공되는 기능으로 쿼리 구현을 method로 한다.

위에서 queryDSL을 사용해서 동적 쿼리를 해결하는 방식은 자세히 들어가면 또 2가지 방식이 있다.

  • BooleanBuilder
  • Where 다중 파라미터 사용

BooleanBuilder 사용

@Test
public void dynamicQuery_BooleanBuilder() throws Exception{
    String usernameParam = "member1";
    Integer ageParam = null;

    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 queryFactory
                .selectFrom(member)
                .where(builder)
                .fetch();
}
  • if 문을 통해 usernameCond, ageCond를 builder.and() 메서드를 통해 조건을 넣어주고 있다.



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


Where 다중 파라미터 사용

@Test
public void dynamicQuery_whereParam() throws Exception{
    String usernameParam = null;
    Integer ageParam = null;

    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 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;
}
  • where 조건에 null 값은 무시된다.
  • usernameEq()과 같은 메서드는 다른 쿼리에서 재활용할 수도 있다.
  • 쿼리 자체의 가독성이 높아진다.

메서드들을 조합해서 사용할 수도 있다.

private Predicate allEq(String usernameCond, Integer ageCond){
    return usernameEq(usernameCond).and(ageEq(ageCond));
}
  • method chaining이 가능하다.
  • null 체크는 주의해서 처리해야 한다.



수정, 삭제 벌크 연산


쿼리 한 번에 대량 데이터 수정

@Test
public void bulkUpdate() throws Exception{
    // member1(10), member2(20) -> 비회원
    // member3(30), member4(40) -> member3, member4
    long count = queryFactory
            .update(member)
            .set(member.username, "비회원")
            .where(member.age.lt(28))
            .execute();

    em.flush();
    em.clear();

    List<Member> result = queryFactory
            .selectFrom(member)
            .fetch();
            
    for (Member member1 : result) {
        System.out.println("member1 = " + member1);
    }
}

기존 숫자에 1 더하기

@Test
public void bulkAdd() throws Exception{
    long count = queryFactory
            .update(member)
            .set(member.age, member.age.add(1))
            .execute();
}
  • 곱하기를 하려면 member.age.multiply(x)를 사용.
  • 빼기를 하려면 member.age.add(-1)를 사용.

쿼리 한 번에 대량 데이터 삭제

@Test
public void buldDelete() throws Exception{
    long count = queryFactory
            .delete(member)
            .where(member.age.lt(18))
            .execute();
}

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



SQL function 호출하기


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


member 👉 M으로 변경하는 replace 함수 사용

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

소문자로 변경해서 비교

String result = queryFactory
.select(member.username)
.from(member)
.where(member.username.eq(Expressions.stringTemplate("function('lower', {0})",
member.username)))

lower 같은 ansi 표준 함수들은 querydsl이 상당 부분 내장하고 있다. 따라서 다음과 같이 처리해도
결과는 같다.

.where(member.username.eq(member.username.lower()))
profile
습관이 전부다.

0개의 댓글