[QueryDSL] 기본 문법
포스트
취소

[QueryDSL] 기본 문법

JPQL과 QueryDSL

  • Querydsl은 JPQL 빌더 역할을 한다.
  • 차이점
    • JPQL
      • 문자열로 작성하기 때문에 실행 시점에 오류를 찾아낸다.
      • 직접 파라미터 바인딩을 해줘야 한다.
    • Querydsl
      • 코드로 작성하기 때문에 컴파일 시점 오류를 찾아낸다.
      • 자동으로 파라미터를 바인딩 해준다.

맛보기

JPQL

String qlString = "select m from Member m  where m.username = :username";

Member findMember =
    em.createQuery(qlString, Member.class)
    .setParameter("username", "member1")
    .getSingleResult();

QueryDSL

  • EntityManagerJPAQueryFactory를 생성한다.
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QMember m = new QMember("m");

Member findMember =
    queryFactory
    .select(m)
    .from(m)
    .where(m.username.eq("member1")) //파라미터 바인딩 처리
    .fetchOne();

JPAQueryFactory를 공통 필드로 사용하기

  • JPAQueryFactory를 필드로 제공할 때 동시성 문제는 걱정하지 않아도 된다.
  • JPAQueryFactory를 생성할 때 제공하는 EntityManager가 해결해준다.
  • 스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공한다.
@PersistenceContext
EntityManager em;

JPAQueryFactory queryFactory;

@BeforeEach
public void before() {
    queryFactory = new JPAQueryFactory(em);
    //...
}

Q-Type 클래스 인스턴스를 사용하는 방법

  • 기본적으로는 인스턴스 방식을 사용한다.
  • 별칭은 서브 쿼리를 작성할 때 주로 사용된다.
QMember qMember = new QMember("m"); //별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용
  • 기본 인스턴스를 static import해서 사용할 수도 있다.
import static study.querydsl.entity.QMember.member;

기본 검색

  • 실제 SQL을 작성하듯이 작성한다.
List<Member> result =
    queryFactory
    .select(member)
    .From(member)
    .fetch();
  • select 메소드와 from 메소드에서 사용되는 Q-Type을 경우에는 selectFrom 메소드를 사용할 수 있다.
List<Member> result =
    queryFactory
    .selectFrom(member)
    .fetch();

조건 검색

  • where(Predicate... o) 메소드를 활용해서 조회 조건을 추가할 수 있다.
  • Predicate 클래스를 통해서 조건을 나타낸다.
  • 만약에 검색 조건 메소드에 파라미터로 들어간 값이 null이라면 해당 조건은 무시된다.
    • 예시 : MyBatis
Member findMember =
    queryFactory
    .selectFrom(member)
    .where(
            member.username.eq("member1")
            ,member.age.eq(10)
    )
    .fetchOne();
select
    m1_0.member_id,
    m1_0.age,
    m1_0.team_id,
    m1_0.username 
from
    member m1_0 
where
    m1_0.username=? 
    and m1_0.age=?
  • and()or()를 통해서 체인을 걸 수 도 있다.
  • Predicate를 쉼표로 구분하면 각 조건문끼리는 AND로 연결된다.
Member findMember =
    queryFactory
    .selectFrom(member)
    .where(
            member.username.eq("member1").and(member.age.eq(10))
    )
    .fetchOne();
select
    m1_0.member_id,
    m1_0.age,
    m1_0.team_id,
    m1_0.username 
from
    member m1_0 
where
    m1_0.username=? 
    and m1_0.age=?
  • WHERE A AND (B OR C)같은 복합 조건도 사용할 수 있다.
Member findMember =
    queryFactory
    .selectFrom(member)
    .where(
            member.username.eq("member1")
            ,member.age.eq(10).or(member.age.eq(20))
    )
    .fetchOne();
select
    m1_0.member_id,
    m1_0.age,
    m1_0.team_id,
    m1_0.username 
from
    member m1_0 
where
    m1_0.username=? 
    and (
        m1_0.age=? 
        or m1_0.age=?
    )

검색 조건 메소드

역할메소드명사용 예시SQL
같은지 비교eqmember.username.eq(“member1”)username = ‘member1’
같지 않은지 비교nemember.username.ne(“member1”)username != ‘member1’
부정 연산notmember.username.eq(“member1”).not()username != ‘member1’
NOT NULL 체크isNotNullmember.username.isNotNull()username is not null
포함 여부 확인inmember.age.in(10, 20)age in (10,20)
미포함 여부 확인notInmember.age.notIn(10, 20)age not in (10, 20)
범위 검색betweenmember.age.between(10, 30)between 10, 30
XX 이상goemember.age.goe(30)age >= 30
XX 초과gtmember.age.gt(30)age > 30
XX 이하loemember.age.loe(30)age <= 30
XX 미만ltmember.age.lt(30)age < 30
패턴에 의한 부분 일치 검색likemember.username.like(“member%”)username like ‘member%’
부분 일치 검색containsmember.username.contains(“member”)username like ‘%member%’
지정 문자열로 시작하는 부분 검색startsWithmember.username.startsWith(“member”)username like ‘member%’
지정 문자열로 끝는 부분 검색endsWithmember.username.endsWith(“member”)username like ‘%member’
  • 이외에도 수많은 검색 조건 메소드가 존재한다.

중복 제거

  • distinct() 메소드를 통해 중복을 제거할 수 있다.
List<String> result = 
    queryFactory
    .select(member.username).distinct()
    .from(member)
    .fetch();

결과 조회

  • fetch()
    • 리스트 조회
    • 결과가 없으면 빈 리스트를 반환한다.
  • fetchOne()
    • 단 건 조회
    • 결과가 없으면 null을 반환한다.
    • 결과가 둘 이상이면 예외를 발생시킨다.
  • fetchFirst()
    • limit(1).fetchOne()을 한 것과 같은 결과를 반환한다.
  • fetchResults()
    • 페이징 정보를 포함한 결과를 반환한다.
    • 총 개수를 조회하는 쿼리도 함께 실행된다.
    • deprecated
  • fetchCount()
    • 총 개수를 조회하는 쿼리로 변환해서 실행한다.
    • deprecated

정렬

  • orderBy(OrderSpecifier&lt;?>... o) 메소드를 통해 정렬한다.
  • orderBy 메소드 안에 정렬 방식을 나열하면 된다.
  • 종류
    • asc()
      • 오름차순
    • desc()
      • 내림차순
    • nullsFirst()
      • 값이 null인 데이터가 전위 정렬된다.
    • nullLast()
      • 값이 null인 데이터가 후위 정렬된다.

페이징

  • offset(long offset)
    • 데이터를 읽어들이기 시작하는 위치를 지정한다.
    • 기본 위치는 0부터 시작한다.
  • limit(long limit)
    • 조회하는 데이터의 건 수를 지정한다.
  • fetchResults()를 통해 페이징 정보를 가져올 수 있다.
    • 다만 deprecated 상태라서 추후 경우에 따라서 따로 처리를 해줘야 할 수도 있다.

그룹 함수

  • 기본적인 그룹 함수
    • 필드.count()
      • 개수 조회
    • 필드.sum()
      • 합산 조회
    • 필드.avg()
      • 평균 조회
    • 필드.max()
      • 최댓값 조회
    • 필드.min()
      • 최솟값 조회
  • 이외에도 다양한 그룹 함수를 제공한다.
  • 그룹 함수를 사용할 때 Tuple을 사용하는 경우가 많다.
@Test
public void aggregation() throws Exception {
    List<Tuple> result =
            queryFactory
            .select(
                    member.count(),
                    member.age.sum(),
                    member.age.avg(),
                    member.age.max(),
                    member.age.min()
            )
            .from(member)
            .fetch();
    
    Tuple tuple = result.get(0);
    
    assertThat(tuple.get(member.count())).isEqualTo(4);
    assertThat(tuple.get(member.age.sum())).isEqualTo(100);
    assertThat(tuple.get(member.age.avg())).isEqualTo(25);
    assertThat(tuple.get(member.age.max())).isEqualTo(40);
    assertThat(tuple.get(member.age.min())).isEqualTo(10);
}

group by와 having

  • groupBy(Expression<?>... o) 메소드를 통해 그룹화를 할 수 있다.
  • groupBy 메소드 안에 그룹화할 대상을 나열하면 된다.
  • having(Predicate... o) 메소드를 통해 그룹화 조건을 지정할 수 있다.
  • having 메소드 안에 그룹화 조건을 나열하면 된다.
@Test
public void group() throws Exception {
    List<Tuple> result =
            queryFactory
            .select(team.name, member.age.avg())
            .from(member)
            .join(member.team, team)
            .groupBy(team.name)
            .having(member.age.gt(20))
            .fetch();
    
    Tuple teamA = result.get(0);
    
    assertThat(teamA.get(team.name)).isEqualTo("teamA");
    assertThat(teamA.get(member.age.avg())).isEqualTo(15);
}

조인 - 기본 조인

  • join(EntityPath<P> target, Path<P> alias) 메소드를 통해 기본 조인을 실행한다.
  • target에는 조인 대상을 지정한다.
  • alias에는 별칭으로 사용할 Q-Type을 지정하면 된다.
  • SQL처럼 ON절을 직접 추가하지 않아도 미리 정의한 연관관계를 통해서 키 값을 매핑하는 ON절을 자동으로 작성해준다.
@Test
public void join() throws Exception {
    List<Member> result =
            queryFactory
            .selectFrom(member)
            .join(member.team, team)
            .where(team.name.eq("teamA"))
            .fetch();
    
    assertThat(result).extracting("username").containsExactly("member1", "member2");
}

조인 - on절

  • on(Predicate... conditions) 메소드를 통해 조인 조건을 추가할 수 있다.
  • on 메소드 안에 조인 조건을 나열하면 된다.
@Test
public void join() throws Exception {
    List<Member> result =
            queryFactory
            .selectFrom(member)
            .join(member.team, team)
            .on(member.age.gt(20))
            .where(team.name.eq("teamA"))
            .fetch();
    
    assertThat(result).extracting("username").containsExactly("member1", "member2");
}

조인 - 페치 조인

  • fetchJoin() 메소드를 통해 페치 조인을 실행할 수 있다.
@Test
public void fetchJoinUse() throws Exception {
    em.flush();
    em.clear();
    
    Member findMember =
            queryFactory
            .selectFrom(member)
            .join(member.team, team).fetchJoin()
            .where(member.username.eq("member1"))
            .fetchOne();
    
    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam()); //EntityManagerFactory를 통해 LAZY 엔티티의 초기화 여부를 알 수 있다.
    
    assertThat(loaded).as("페치 조인 적용").isTrue();
}

서브 쿼리

  • 기본적으로 Q-Type 클래스는 인스턴스를 사용하는 방법이 두 가지가 있다.
    • QMember qMember = new QMember("m"); //별칭 직접 지정
    • QMember qMember = QMember.member; //기본 인스턴스 사용
  • 서브 쿼리를 사용할 때는 별칭 방식을 사용해야 한다.
  • JPAExpressions를 통해 서브 쿼리를 작성한다.
    • 서브 쿼리를 사용하는 방식 자체는 메인 쿼리와 별차이가 없다.
@Test
public void subQuery() throws Exception {
    QMember memberSub = new QMember("memberSub");
    
    List<Member> result =
            queryFactory
            .selectFrom(member)
            .where(
                member.age.eq(
                    JPAExpressions
                    .select(memberSub.age.max())
                    .from(memberSub)
                )
            )
            .fetch();
    
    assertThat(result).extracting("age").containsExactly(40);
}

Case문

  • 필드에서 직접 사용한다.

  • 단순한 방식

member.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타")
  • 복잡한 방식
    • 복잡한 변수를 사용할 때는 변수로 따로 선언해서 사용하는 것이 좋다.
new CaseBuilder()
.when(member.age.between(0, 20)).then("0~20살")
.when(member.age.between(21, 30)).then("21~30살")
.otherwise("기타")

상수 사용하기

  • 상수를 표현해야 하는 경우에 사용한다.
  • Expressions.constant(T value) 메소드 안에 상수로 표현할 값을 명시하면 된다.
List<Tuple> result =
    queryFactory
    .select(member.username, Expressions.constant("A"))
    .from(member)
    .fetch();

문자 더하기

  • concat(String str) 메소드를 통해 문자열을 합칠 수 있다.
List<String> result =
    queryFactory
    .select(member.username.concat("_").concat(member.age.stringValue()))
    .from(member)
    .where(member.username.eq("member1"))
    .fetch();

출처

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.