Spring Framework/[인프런] 실전! QueryDSL

Chapter 02. 기본 문법

계란💕 2023. 4. 13. 00:06

QueryDSL vs JPQL

 

  Ex) member1 찾기

  • JPQL: Stiring 형태로 쿼리를 직접 작성하고 파라미터를 세팅한다. (setParameter())

 

  • QueryDSL: QMember라는 클래스가 있는데 이는 Member를 엔티티로 등록했기 때문에 자동으로 생성된 Q 클래스이다. QueryFactory 구문에서 FROM 절에서 테이블처럼 QMember 인스턴스를 쓸 수 있다. 
    • 파라미터를 바인딩 처리한다. 
<hide/>

@Test
public void startJPQL() {

    String qlString = "SELECT m FROM Member m "
        + "WHERE m.username = :username";
    Member findMember = em.createQuery(qlString, Member.class)
        .setParameter("username", "member1")
        .getSingleResult();
    assertThat(findMember.getUsername()).isEqualTo("member1");
}

@Test
public void startQueryDSL() {

    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    QMember m = new QMember("m");
    Member findMember = queryFactory.select(m)
        .from(m)
        .where(m.username.eq("member1"))
        .fetchOne();

    assertThat(findMember.getUsername()).isEqualTo("member1");
}

 

  Note)

  • 공통점: EntityManager로 JPAQueryFactory 를 생성한다. 
  • 차이점
    • JPQL: 문자 (실행 시점 오류),  파라미터 바인딩을 직접 해야한다. 
    • QueryDSL: 코드(컴파일 시점에 바로 오류 발생), 파라미터 바인딩을 자동으로 처리한다. 

 JPAQueryFactory를 필드로

 

  Ex) 팩토리를 필드로 선언하고 member1을 찾기

  • JPAQueryFactory를 필드로 제공하면 동시성 문제는?
    • 동시성 문제는 JPAQueryFactory를 생성할 떄 제공하는 EntityManager에 달려있다. 
    • 스프링 프레임워크는 여러 스레드가 동시에 같은 EntityManager에 접근하더라도 트랜잭션마다 별도의 영속성 컨텍스트를 제공하므로 동시성 문제를 걱정하지 않아도 된다. 
<hide/>
@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    @PersistenceContext
    EntityManager em;

    JPAQueryFactory queryFactory;

    @BeforeEach
    public void before(){
        queryFactory = new JPAQueryFactory(em);
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);
        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);
        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);
        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);;
    }


    @Test
    public void startQueryDSL2() {
        QMember m = new QMember("m");
        Member findMember = queryFactory.select(m)
            .from(m)
            .where(m.username.eq("member1"))
            .fetchOne();
        assertThat(findMember.getUsername()).isEqualTo("member1");
    }
}

기본 Q-Type 활용

 

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

QMember qMember1 = new QMember("m");    // 별칭 직접 지정
QMember qMember2 = QMember.member;  // 기본 인스턴스 사용

 

 

 

기본 인스턴스를 static import 와 함께 사용

  • public static final QMember member = new QMember("member1");
    • QMember 클래스에 위와 같은 static 변수가  선언되어 있다.    
<hide/>
import static study.querydsl.entity.QMember.member;

@Test
public void startQueryDSL3() {
    Member findMember = queryFactory.select(member)
        .from(member)
        .where(member.username.eq("member1"))
        .fetchOne();
    assertThat(findMember.getUsername()).isEqualTo("member1");
}

 

 

  • 다음과같이 yml 파일에 설정해주면 JPQL 이 어떻게 실행되는지 쿼리를 볼 수 있다. 
spring: 
  jpa:
    properties:
      hibernate:
        use_sql_comments: true

기본 검색 쿼리

 

<hide/>
@Test
public void search() {
    Member findMember = queryFactory
        .selectFrom(member)
        .where(member.username.eq("member1")
        .and(member.age.eq(10)))
        .fetchOne();
    assertThat(findMember.getUsername()).isEqualTo("member1");
}

 

  • 검색 조건은 위와 같이 and(), or()을 메서드 체인으로 연결 가능하다. 
  • selectFrom() = select() + from()
  • and 를 쓰지 않으면 where 절 안에 나열해서 콤마로 구분해서 조건을 넣을 수 있다. 

cf)  쿼리 DSL 검색 조건 메서드

  • member.username.eq("member1')
  • member.username.eq("member1").not():  username != member1
  • member.username.ne():  username != member1 (위와 같음)
  • member.age.isNotNull(): not null인 것만 가져온다. 
  • member.age.in(10, 20)
  • member.age.notIn()
  • between()
  • member.age.goe(): age >= 30
    • goe(greater than or equal to): 이상 
  • member.age.gt(): age > 30
    • gt(greater than): 초과
  • member.age.loe(): age <= 30
    • loe(lesser than or equal to): 이하 
  • member.age.lt():  age < 30
    • lt(lesser than): 미만
  • like("member%")
  • contains("member")
  • startsWith("member")

cf) Query DSL 결과 조회 메서드

  • fetch(): 리스트로 결과를 반환한다. 없으면 빈 리스트 반환
  • fetchOne(): 단 건을 조회하는 경우에 사용한다. 한 번 호출할 때 하나의 row만 가져온다. 
    • 결과 없으면 null
    • 둘 이상이면: NonUniqueResultException 
  • fetchFirst(): 처음 한 건을 쿼리해서 가져온다. 
    • limit(1).fetchOne()
  • fetchResults(): 해당 내용을 페이징을 위해 사용 가능하다. 
    • 페이징 정보 포함, total count 쿼리 추가 실행
  • fetchCount(): COUNT 쿼리로 변경해서 count수를 조회한다. 
  • fetchAll(): 모든 데이터를 클라이언트로 한 번에 가져온다. 

정렬

 

  Ex) 정렬 

<hide/>
@Test
public void sort() {
    em.persist(new Member(null, 100));
    em.persist(new Member("member5", 100));
    em.persist(new Member("member6", 100));

    List<Member> result = queryFactory.selectFrom(member)
        .where(member.age.eq(100))
        .orderBy(member.age.desc(), member.username.asc().nullsLast())
        .fetch();

    Member member5 = result.get(0);
    Member member6 = result.get(1);
    Member memberNull = result.get(2);
    assertThat(member5.getUsername()).isEqualTo("member5");
    assertThat(member6.getUsername()).isEqualTo("member6");
    assertThat(memberNull.getUsername()).isNull();
}
  • 회원 나이 내림차순, 이름 오름차순, 회원이름이 없으면 마지막에 출력되도록(nullLast()) 한다. 
  • nullLast(), nullFirst() 를 이용해서 null 값에 대한 순서를 부여할 수 있다. 

페이징

  • offset(1): 0부터 시작한다. zero index
  • limit(2): 2건 조회 
  • queryResults.getTotal()은 페이징 조건을 적용하기 전에 모든 row()의 결과를 반환한다. 
  • queryResults.getResults().size():  페이징 조건을 적용한 다음의 결과 row() 수를 반환한다. 
<hide/>
@Test
public void paging() {

    QueryResults<Member> queryResult = queryFactory
        .selectFrom(member)
        .orderBy(member.username.desc())
        .offset(1)
        .limit(2)
        .fetchResults();

    assertThat(queryResult.getTotal()).isEqualTo(4);
    assertThat(queryResult.getLimit()).isEqualTo(2);
    assertThat(queryResult.getOffset()).isEqualTo(1);
    assertThat(queryResult.getResults().size()).isEqualTo(2);
}

 

  Note) 

  • 위와 같이 실행하면  count() 쿼리가 실행되므로 성능 문제에 주의한다. 
  • 실무에서 페이징 쿼리를 작성할 때 데이터 조회하는 쿼리는 여러 테이블을 조회하지만 count 쿼리는 조인이 필요없는 경우도 있다. 자동화된 count 쿼리는 원본 쿼리와 같이 모두 조인을 해버리기 때문에 성능이 안 나올 수 있다. 
  • count  쿼리에 조인이 필요없는 성능 최적화가 필요하다면 count 전용 쿼리를 별도로 작성해야한다.

집합 함수

  • COUNT(m): 회원 수
  • SUM(): 나이 
  • AVG(): 평균
  • MAX(): 최대 
  • MIN(): 최소 
  • groupBy(), having()

JOIN - 기본 조인

  • join(조인 대상, 별칭으로 사용할 Q 타입)
  • leftJoin()
  • rightJoin()
  • on()

세타 조인(theta join)

  • 연관 관계가 없는 필드로 조인
  • 다음과 같이 from  절 안에 매개변수로 여러 개의 엔티티를 넣어서 세타 조인할 수 있다. 
  • ON 절을 이용하면 조인 대상을 필터링하고 연관관계 없는 엔티티를 외부 조인할 수 있다. 
<hide/>
@Test
public void theta_join() throws Exception {

    em.persist(new Member("teamA"));
    em.persist(new Member("teamB"));

    List<Member> result = queryFactory
        .select(member)
        .from(member, team)
        .where(member.username.eq(team.name))
        .fetch();

    assertThat(result).extracting("username")
        .containsExactly("teamA", "teamB");


}

페치 조인(fetch join)

  • 페치 조인은 SQL 조인을 활용해서 연관된 엔티티를 한 번에 조회하는 기능이다. 
  • 주로 성능 최적화를 위해 사용한다. 
  • 페치 조인은   SQL에서 제공하는 기능은 아니다. 

 

 

  Ex) 페치 조인 미적용

<hide/>
@Test
public void fetchJoinNo() throws Exception {

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

    Member findMember = queryFactory
        .selectFrom(member)
        .where(member.username.eq("member1"))
        .fetchOne();

    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertThat(loaded).as("페치 조인 미적용").isFalse();

}
  • 지연로딩으로 Member, Team SQL 쿼리를 각각 실행한다. 

 

  Note) 실행 결과

<hide/>
/* select member1
from Member member1
where member1.username = ?1 */ select member0_.id as id1_1_, member0_.age as age2_1_, member0_.team_id as team_id4_1_, member0_.username as username3_1_ from member member0_ where member0_.username=?
/* select member1
from Member member1
where member1.username = 'member1'1 */ select member0_.id as id1_1_, member0_.age as age2_1_, member0_.team_id as team_id4_1_, member0_.username as username3_1_ from member member0_ where member0_.username=NULL;

 

 

 

  Ex) 페치 조인 적용

<hide/>
@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());
    assertThat(loaded).as("페치 조인 적용").isTrue();

}
  • join() 메서드 뒤에 fetchJoin()을 추가한다. 

 

  Note) 실행 결과

<hide/>
/* select member1
from Member member1
  inner join fetch member1.team as team
where member1.username = ?1 */ select member0_.id as id1_1_0_, team1_.id as id1_2_1_, member0_.age as age2_1_0_, member0_.team_id as team_id4_1_0_, member0_.username as username3_1_0_, team1_.name as name2_2_1_ from member member0_ inner join team team1_ on member0_.team_id=team1_.id where member0_.username=?
/* select member1
from Member member1
  inner join fetch member1.team as team
where member1.username = 'member1'1 */ select member0_.id as id1_1_0_, team1_.id as id1_2_1_, member0_.age as age2_1_0_, member0_.team_id as team_id4_1_0_, member0_.username as username3_1_0_, team1_.name as name2_2_1_ from member member0_ inner join team team1_ on member0_.team_id=team1_.id where member0_.username=NULL;

 


서브 쿼리

  • 모든 테스트는  앞에서 살펴본 before() 메서드를 실행한 다음에 실행된다는 점을 염두에 둬야한다.  (@BeforeEach 애애너테이션이 있기 때문)
  • JPAExpressions: 다양한 유형의 서브쿼리를 작성할 수 있는 API
    • IN 서브쿼리, EXISTS 서브쿼리, FROM 서브쿼리
    • 복잡한 서브쿼리를 간단하게 작성 가능하다. 

 

 

  Ex 1) eq , MAX 사용해서 나이가 가장 많은 멤버 조회하기

<hide/>
@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);
}

 

 

 

  Ex 2) goe, AVG 사용해서 나이가 평균 이상인 회원 조회하기

<hide/>
@Test
public void subQueryGoe() throws Exception {

    QMember memberSub = new QMember("memberSub");

    List<Member> result = queryFactory
        .selectFrom(member)
        .where(member.age.goe(JPAExpressions
            .select(memberSub.age.avg())
            .from(memberSub)))
        .fetch();

    assertThat(result).extracting("age")
        .containsExactly(30, 40);

}
  • goe (greater or equal)

 

 

  Ex 3) 여러 건 처리 in 사용

<hide/>
@Test
public void subQueryIn() throws Exception {

    QMember memberSub = new QMember("memberSub");

    List<Member> result = queryFactory
        .selectFrom(member)
        .where(member.age.in(
            JPAExpressions
                .select(memberSub.age)
                .from(memberSub)
                .where(memberSub.age.gt(10))))
        .fetch();

    assertThat(result).extracting("age")
        .containsExactly(20, 30, 40);
}
  • gt(greater than)이므로 10을 초과하는 20, 30, 40이 결과로 추출된다.

 

 

  Ex) SELECT 절 안에 서브쿼리 

<hide/>
QMember memberSub = new QMember("memberSub");
List<Tuple> fetch = queryFactory.select(member.username,
        JPAExpressions.select(memberSub.age.avg())
            .from(memberSub)
    ).from(member)
    .fetch();

for (Tuple tuple : fetch) {
    System.out.println("username = " + tuple.get(member.username));
    System.out.println(
        "age = " + tuple.get(JPAExpressions
            .select(memberSub.age.avg())
            .from(memberSub)));
}

 

  Note) 실행 결과

 


FROM  절의 서브쿼리 한계

  • JPA  JPQL 서브쿼리의 한계점으로 FROM 절의 서브쿼리(인라인 뷰)는 지원하지않는다. 
  • QueryDsl도 지원하지 않는다. 
  • 하이버네이트 구현체를 사용하면 SELECT 절의 서브쿼리는 지원한다. 
  • QueryDsl도 하이버네이트 구현체를 사용하면 SELECT 절의 서브쿼리를 지원한다. 

 

 

FROM  절의 서브쿼리 해결 방안

  • 서브쿼리JOIN으로 변경한다. 
  • 애플리케이션에서 쿼리를 2번 분리해서 실행한다. 
  • NativeSQL을 사용한다. 

CASE 

  • CASE문은 SELECT, WHERE, ORDRE BY 절에서 사용 가능하다. 

 

 

  Ex 1) 단순 조건

  •  age = 10 처럼 딱 떨어지는 값일 때,  다음과 같이 쓸 수 있다. 
<hide/>
List<String> result1 = queryFactory.select(
    member.age
        .when(10).then("열살")
        .when(20).then("스무살")
        .otherwise("기타"))
.from(member)
.fetch();

 

 

  Ex 2) 복잡 조건

  • SELECT 절 안에 CaseBuilder를 이용해서 회원 나이 조건을 넣어준다. 
<hide/>
List<String> result2 = 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();

 

 

  Ex 3) OrderBy 절에서 CASE문 함께 사용하기 

  • 다음 우선 순위에 맞춰서 출력하라
    • 0 ~ 30 살이 아닌 회원을 가장 먼저 출력
    • 0  ~ 20 살 회원 출력
    • 21 ~ 30 살 회원 출력
  • 다음과 같이  CaseBuilder()를 이용해서 경우에 따라 순위를 나타내는 번호를 부여한다. 
    • List<Tuple> 에 테이블 정보와 rankPath를 같이 select 해주면 각 row에 맞는 순위 번호가 들어가고 이에 따라 내림차순 정렬된다. 
    • 30 세 초과인 사람이 가장 우선 순위 이므로 othrewise에 포함시킨다. 그 다음에 desc 내림차순 정렬하면 3순위가 장 먼저 출력.
<hide/>
// orderBy에서 Case문 함께 사용하기
NumberExpression<Integer> rankPath = new CaseBuilder()
    .when(member.age.between(0, 20)).then(2)
    .when(member.age.between(21, 30)).then(1)
    .otherwise(3);
List<Tuple> result3 = queryFactory
    .select(member.username, member.age, rankPath)
    .from(member)
    .orderBy(rankPath.desc())
    .fetch();
for(Tuple tuple : result3){
    String username = tuple.get(member.username);
    Integer age = tuple.get(member.age);
    Integer rank = tuple.get(rankPath);
    System.out.println("username = " + username + " age = " + age + " rank = " + rank);

 

 Note) 실행 결과

  • rankPath 처럼 복잡한 조건을 변수로 선언해서 SELECT 절과 OrderBy 절에서 함께 사용 가능하다. 
  • 설정한 순위에 대해 내림차순 정렬을 했기 때문에 rank가 후순위인 row부터 출력된다.
<hide/>
username = member4 age = 40 rank = 3
username = member1 age = 10 rank = 2
username = member2 age = 20 rank = 2
username = member3 age = 30 rank = 1

 


상수, 문자 더하기

 

  Ex) 상수와 문자 이어 붙이기

 

  • 어떤 상수를 함께 출력하기 위해 다음과 같이 Expressions.constant()를 이용할 수 있다. 
<hide/>
Tuple result = queryFactory.select(member.username, Expressions.constant("A"))
    .from(member)
    .from(member)
    .fetchFirst();

 

  • concat()를 이용하면 문자열을 이어붙일 수 있다. 
<hide/>
String result = queryFactory
    .select(member.username.concat("_").concat(member.age.stringValue()))
    .from(member)
    .where(member.username.eq("member1"))
    .fetchOne();

 

 

출처 -  https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84

 

실전! Querydsl - 인프런 | 강의

Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!, - 강의 소개 | 인프런

www.inflearn.com