Spring Boot/QueryDSL

Spring Boot QueryDSL 중급 문법

최-코드 2024. 8. 24. 09:34

프로젝션과 결과 반환

  • 프로젝션 대상(select 문 인자)이 하나이면 해당하는 대상의 타입으로 받으면 된다.
  • 대상이 둘 이상일 경우, Tuple 혹은 Dto로 받아야 된다.
  • Tuple의 경우 repository 계층에서만 사용하고 service 계층으로 안 넘어오도록 하자.
@Test
public void simpleProjection(){
    List<String> fetch = queryFactory
            .select(member.username)
            .from(member)
            .fetch();

    for(String s : fetch){
        System.out.println("s="+s);
    }
}

@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);
        System.out.println(age);
    }
}
  • Dto로 바로 반환하는 방법으로는 총 3가지가 있다.
    • 프로퍼티 접근 - Setter 이용
    • 필드 직접 접근
      • 필드에 바로 값 할당한다.
      • private이어도 상관없이 자바에서 알아서 해준다.
    • 프로퍼티나 필드 접근에 대해서 dto의 필드 이름과 entity의 필드 이름이 같아야 한다. 기본적으로 jpql이므로 테이블의 컬럼명이 아닌 클래스의 필드명이랑 같아야 한다. 맞지 않으면 오류는 없어도 null값으로 할당이 된다. 만약 이름이 다를 때는 .as("~")을 통해 맞춰줄 수 있다.
    • 생성자 사용 - 생성자 인자의 타입과 딱딱 맞아야 한다. 그래야 오류없이 실행된다.
    • ExpressionUtils.as(source, alias)를 통해 서브쿼리에 별칭을 적용할 수 있다.
@Test
public void findDtoBySetter() {
    List<MemberDto> fetch = queryFactory
            .select(Projections.bean(MemberDto.class,
                    member.username, member.age))
            .from(member)
            .fetch();

    for (MemberDto memberDto : fetch) {
        System.out.println(memberDto);
    }
}

@Test
public void findDtoByField() {
    List<MemberDto> fetch = queryFactory
            .select(Projections.fields(MemberDto.class,
                    member.username, member.age))
            .from(member)
            .fetch();

    for (MemberDto memberDto : fetch) {
        System.out.println(memberDto);
    }
}

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

@Test
    public void findMaxDto() {
        QMember memberSub = new QMember("memberSub");
        queryFactory
                .select(Projections.fields(MemberDto.class,
                                member.username,
                                ExpressionUtils.as(
                                        JPAExpressions
                                                .select(memberSub.age.max())
                                                .from(memberSub), "age")
                        )
                ).from(member)
                .fetch();
}

@Test
public void findDtoByConstructor() {
    List<MemberDto> fetch = queryFactory
            .select(Projections.constructor(MemberDto.class,
                    member.username, member.age)) //만약 int가 아닌 long일시 에러 발생
            .from(member)
            .fetch();

    for (MemberDto memberDto : fetch) {
        System.out.println(memberDto);
    }
}
  • @QueryProjection 어노테이션을 Dto의 생성자 위에 붙이기
    • 해당 Dto를 QClass로 만들어준다.
    • 이 때 주의할 사항으로는 다시 컴파일해야 QClass가 생성된다.
    • 위의 생성자 사용으로부터 장점은 인자에 대해 컴파일 타임 때 오류를 찾을 수 있다. 위의 생성자 사용은 런타임 때 인자의 타입이나 개수에 대해 오류를 보낸다.
    • 위의 생성자 사용과 마찬가지로 실제 생성자 메소드를 호출한다.
    • 단점으로는 QClass가 생성되는 것과 dto가 QueryDSL을 의존한다는 점이다.
@Test
public void findDtoByQueryProjection() {
    List<MemberDto> fetch = queryFactory
            .select(new QMemberDto(member.username, member.age))
            .from(member)
            .fetch();

    for (MemberDto memberDto : fetch) {
        System.out.println(memberDto);
    }
}

 

동적 쿼리

  • null에 대해 처리해줄 때 유용하다.
  • BooleanBuilder 사용하여 동적 쿼리 만들기
@Test
public void dynamicQuery_BooleanBuilder(){
    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();
}
  • BooleanBuilder 생성자를 호출할 때 인자로 초기 boolean 값을 넣어줄 수 있다.
  • where 다중 파라미터 사용하여 동적 쿼리 만들기 (추천)
    • 가독성이 올라간다.
    • 조건끼리 조합으로 새로운 조건을 만들 수 있다.
    • 원래 존재하는 함수를 다른 쿼리에도 또 이용할 수 있다. 즉, 재사용성이 올라간다.
    • 비교에 대한 함수의 리턴타입은 BooleanExpression으로 하자. Predicate일시에는 .and()로 새로운 조건을 만들지 못한다.
@Test
    public void dynamicQuery_whereParam() {
        String usernameParam = "member1";
        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) {
        //식이 간단할 떄는 삼항 연산자 사용 아니면 if문 사용
        return usernameCond != null ? member.username.eq(usernameCond) : null;
    }

    private BooleanExpression ageEq(Integer ageCond) {
        return ageCond != null ? member.age.eq(ageCond) : null;
    }

    //where를 통한 동적쿼리는 아래와 같이 조합을 통해 새로운 where 조건을 구할 수 있다.
    private BooleanExpression allEq(String usernameCond, Integer ageCond){
        return usernameEq(usernameCond).and(ageEq(ageCond));
    }

 

수정, 삭제 배치 쿼리

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

    em.clear();

    List<Member> result = queryFactory
            .selectFrom(member)
            .fetch();

    for (Member member1 : result) {
        System.out.println(member1.getUsername());
    }
}

@Test
public void bulkAdd(){
    queryFactory
            .update(member)
            .set(member.age, member.age.add(1)/*.multiply(2)*/)
            .execute();
}

@Test
public void bulkDelete(){
    queryFactory
            .delete(member)
            .where(member.age.gt(18))
            .execute();
}