Post

Querydsl

Querydsl 기본 문법

기본 코드

1
2
3
4
@PersistenceContext
EntityManager em;
// Querydsl 셋팅 - 엔티티매니저를 주입해 JPAQueryFactory 생성
JPAQueryFactory queryFactory = new JPAQueryFactory(em);

Q-Type

  • Querydsl은 @Entity 붙은 클래스들의 Q-Type이라는 클래스를 만들어줌
  • 컴파일 시 생성되어 Querydsl 사용시 QMember와 같은 Q-Type을 사용해야한다.

결과 조회

  • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환

  • fetchOne() : 단 건 조회

    • 결과가 없으면 : null

    • 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException

  • fetchFirst() : limit(1).fetchOne()

페이징

1
2
3
4
5
6
List<Member> result = queryFactory 
            .selectFrom(member) 
            .orderBy(member.username.desc()) 
            .offset(1) //0부터 시작(zero index) 
            .limit(2) //최대 2건 조회 
            .fetch();
  • 위와 같이 offset, limit 메소드를 활용하면 된다.

  • Pageable를 사용해서도 가능하다.

    1
    2
    3
    4
    5
    6
    
    List<Member> result = queryFactory 
                .selectFrom(member) 
                .orderBy(member.username.desc()) 
                .offset(pageable.getOffset()) //0부터 시작(zero index) 
                .limit(pageable.getPageSize()) //최대 2건 조회 
                .fetch();
    

기본 조인

1
join(조인 대상, 별칭으로 사용할 Q타입)
  • innerJoin(), leftJoin(), rightJoin() 모두 지원한다.

    1
    2
    3
    4
    5
    
    List<Member> result = queryFactory 
                .selectFrom(member) 
                .join(member.team, team) 
                .where(team.name.eq("teamA")) 
                .fetch();
    

세타 조인

  • 연관관계가 없는 필드로 조인

    1
    2
    3
    4
    
    queryFactory 
    	.select(member)
        .from(member, team)
        // 나머지 쿼리
    
  • from 절에 여러 엔티티 선택해서 세타 조인
  • 외부 조인 불가능 -> 조인 on 사용하면 외부 조인 가능

조인 - on

  • ON절을 활용한 조인

    • 조인 대상 필터링

      1
      2
      3
      4
      5
      6
      
      //  회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
      List<Tuple> result = queryFactory
                  .select(member, team) 
                  .from(member)
                  .leftJoin(member.team, team).on(team.name.eq("teamA")) 
                  .fetch();
      
      • on 절을 활용해 조인 대상을 필터링 할 때, 외부 조인이 아니라 내부 조인을 사용하면 where 절에서 필터링하는 것과 기능이 동일함
      • 내부 조인은 where절을 이용하고, on 절은 외부조인이 필요한 경우에만 이 기능을 사용하자.
    • 연관관계 없는 엔티티 외부 조인

      1
      2
      3
      4
      5
      6
      
      //회원의 이름과 팀의 이름이 같은 대상 외부 조인
      List<Tuple> result = queryFactory 
                  .select(member, team) 
                  .from(member)
                  .leftJoin(team).on(member.username.eq(team.name)) 
                  .fetch();
      
      • on을 사용해서 서로 관계가 없는 필드로 외부 조인하는 것이 가능
      • 주의! leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어감
      • 일반조인: leftJoin(member.team, team)
      • on조인: from(member).leftJoin(team).on(xxx)

페치 조인

  • JPA에서 사용하는 페치 조인과 같은 기능을 제공한다.

    1
    2
    3
    4
    5
    
    Member findMember = queryFactory 
                .selectFrom(member)
                .join(member.team, team).fetchJoin() 
                .where(member.username.eq("member1")) 
                .fetchOne();
    

서브 쿼리

  • com.querydsl.jpa.JPAExpressions 사용

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    // 나이가 가장 많은 회원 조회
    List<Member> result = queryFactory
                .selectFrom(member) 
                .where(member.age.eq(
    					JPAExpressions
                                .select(memberSub.age.max()) 
                                .from(memberSub) 
                ))
                .fetch();
    
  • JPAExpressions.쿼리… 와 같이 서브 쿼리를 작성하면 된다.

Case 문

  • select, where, order by에서 사용 가능

  • 단순 조건

    1
    2
    3
    4
    5
    6
    7
    
    List<String> result = queryFactory 
            .select(member.age 
                    .when(10).then("열살") 
                    .when(20).then("스무살") 
                    .otherwise("기타")) 
            .from(member)
            .fetch();
    
  • 복잡한 조건

    1
    2
    3
    4
    5
    6
    7
    
    List<String> result = queryFactory 
            .select(new CaseBuilder()
                    .when(member.age.between(0, 20)).then("0~20살") 
                    .when(member.age.between(21, 30)).then("21~30살") 
                    .otherwise("기타"))
            .from(member) 
            .fetch();
    
  • orderBy에서 Case 문 사용하기

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    // case 문 변수로 사용
    NumberExpression<Integer> rankPath = new CaseBuilder() 
            .when(member.age.between(0, 20)).then(2) 
            .when(member.age.between(21, 30)).then(1) 
            .otherwise(3);
      
    // order by 절에 적용
    List<Tuple> result = queryFactory
            .select(member.username, member.age, rankPath) 
            .from(member)
            .orderBy(rankPath.desc()) 
            .fetch();
    

상수, 문자 더하기

  • 상수가 필요하면 Expressions.constant(xxx)로 사용할 수 있음

    1
    2
    3
    4
    5
    
    Tuple result = queryFactory
            .select(member.username, Expressions.constant("A")) 
            .from(member)
            .fetchFirst();
    // 결과 : [이름, A] 
    
  • 문자 더하기 concat

    1
    2
    3
    4
    5
    6
    
    String result = queryFactory
            .select(member.username.concat("_").concat(member.age.stringValue())) 
            .from(member)
            .where(member.username.eq("member1")) 
            .fetchOne();
    // 결과 : member1_10
    
    • 참고 : member.age.stringValue() 부분이 중요한데, 문자가 아닌 다른 타입들은 stringValue()로 문자로 변환 가능. 주로 ENUM 처리할 때 사용

중급 문법

결과를 DTO로 반환 (Bean population)

  • 프로퍼티 접근(setter)

    1
    2
    3
    
    select(Projections.bean(MemberDto.class, 
                    member.username, 
                    member.age)
    
  • 필드 직접 접근

    1
    2
    3
    
    Projections.fields(MemberDto.class, 
                    member.username,
                    member.age)
    
  • 생성자 접근

    1
    2
    3
    
    Projections.constructor(MemberDto.class, 
                    member.username,
                    member.age)
    
  • 중간에 서브쿼리 사용

    1
    2
    3
    4
    5
    6
    
    Projections.constructor(MemberDto.class, 
                    member.username,
                    ExpressionUtils.as(
                    	JPAExpressions
                        	.select(memberSub.age.max())
                        	.from(memberSub), "age"))
    
    • ExpressionUtil.as(source, alias) : 필드나, 서브쿼리에 별칭 적용
    • username.as("memberName") : 필드에 별칭 적용
  • @QueryProjection : DTO 생성자에 어노테이션 붙여서 사용

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    // DTO 생성자
    @QueryProjection
    public MemberDto(String username, int age) { 
    	this.username = username;
    	this.age = age;
    }
      
    // @QueryProjection 활용
    List<MemberDto> result = queryFactory
            .select(new QMemberDto(member.username, member.age)) 
            .from(member)
            .fetch();
    
    • 이 방법은 컴파일러로 타입을 체크할 수 있는 가장 안전한 방법. 다만 DTO에 Querydsl 어노테이션을 유지해야하므로 DTO가 Querydsl을 의존한다는 단점이 있다.

동적쿼리

  • Querydsl은 where절에 조건을 추가함으로써 동적 쿼리를 쉽게 사용할 수 있다.

  • BooleanBuilder

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    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()에 .and로 조건 추가
  • where 다중 파라미터 사용

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    private List<Member> searchMember2(String usernameCond, Integer ageCond) { 
    return queryFactory
                .selectFrom(member)
                .where(usernameEq(usernameCond), ageEq(ageCond)) // null이면 통과
                .fetch();
    }
      
    // 조건 메소드
    private BooleanExpression usernameEq(String usernameCond) {
    	return StringUtils.hasText(usernameCond) ? member.username.eq(usernameCond) : null;
        //return usernameCond != null ? member.username.eq(usernameCond) : null;
    }
      
    private BooleanExpression ageEq(Integer ageCond) {
    	return ageCond != null ? member.age.eq(ageCond) : null; 
    }
    
    • where 조건에 null은 무시
    • 메소드를 이용하면 다른 곳에서 재사용 가능
    • 가독성이 좋음
    • String의 경우 null로 체크를 하면 ““과 같은 것을 처리하지 못한다. -> StringUtils.hasText(xxx)로 처리하는게 알맞다.

수정, 삭제 벌크 연산

  • 벌크 쿼리

    1
    2
    3
    4
    5
    
    long count = queryFactory 
            .update(member)
            .set(member.username, "비회원") 
            .where(member.age.lt(28)) 
            .execute();
    
  • .execute() 를 사용해 벌크 쿼리를 사용 가능

출처)

[실전! Querydsl 강의대시보드 - 인프런 (inflearn.com)](https://www.inflearn.com/course/querydsl-실전/dashboard)
This post is licensed under CC BY 4.0 by the author.