[Query DSL] 기본 문법 ①

홍정완·2022년 8월 10일
0

JPA

목록 보기
24/38
post-thumbnail

데이터 셋업


@BeforeEach
public void before() throws Exception {
    
    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);

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

    List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
    for (Member member : members) {
        System.out.println("member = " + member);
        System.out.println("member.getTeam() = " + member.getTeam());
    }
}



JPQL vs Querydsl


@Autowired EntityManager em;

@Test
public void startJPQL() throws Exception {
    
    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() throws Exception {
	
    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");
}
  • EntityManager를 주입해서 JPAQueryFactory를 생성한다.
  • Querydsl은 JPQL 빌더
  • JPQL : 문자(런타임 오류) vs Querydsl : 코드(컴파일 시점 오류)
  • JPQL : 직접 파라미터 바인딩 vs Querydsl : 자동 파라미터 바인딩 자동



JPAQueryFactory 필드로 빼기


@Autowired
EntityManager em;

JPAQueryFactory queryFactory;

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

@Test
public void startJPQL() throws Exception {
    //member1 find
    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() throws Exception {
    //given
    Member findMember = queryFactory
            .selectFrom(member)
            .where(member.username.eq("member1")
                    .and(member.age.eq(10))) 
            .fetchOne();
    assertThat(findMember.getUsername()).isEqualTo("member1");
}

✅ JPAQueryFactory를 필드로 제공하면 동시성 문제(Multi Threading)은 어떻게 될까 ❓

👉 스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도, 트랜잭션마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정하지 않아도 된다.



기본 Q-Type 활용


위의 Querydsl 예제에서는 Q 클래스의 인스턴스를 사용할 때 new QMember("m")와 같이 new를 통해 별칭을 직접 지정해 줬지만, Querydsl에서 제공하는 기본 인스턴스를 사용하는 게 더 간편하다.

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

  • Q 클래스를 static-import 해주면 member도 바로 사용할 수 있다.
import static study.querydsl.entity.QMember.*;

@Test
public void startQuerydsl3(){
		Member findMember = queryFactory.selectFrom(member).where(member.id.eq(1L).fetchOne();
		assertThat(findMember.getId)).isEqualTo(1L);
}



검색 조건 쿼리


기본 검색 쿼리

@Test
public void searchAndParam() throws Exception {
   
    Member findMember = queryFactory
            .selectFrom(member)
            .where(
                    member.username.eq("member1")
                    .and(member.age.eq(10))
            ) 
            .fetchOne();
            
    assertThat(findMember.getUsername()).isEqualTo("member1");
}
  • 검색 조건은 .and(), .or()를 메서드 체인으로 연결할 수 있다.
  • selectfromselectFrom으로 합칠 수 있다.



AND 조건을 메서드 체인 말고 파라미터로 처리할 수도 있다.

@Test
public void searchAndParam() throws Exception {

    Member findMember = queryFactory
            .selectFrom(member)
            .where(
                    member.username.eq("member1"),
                    member.age.eq(10)
            ) 
            .fetchOne();
            
    assertThat(findMember.getUsername()).isEqualTo("member1");
}
  • null 값은 무시한다. 👉 동적 쿼리 생성에 용이하다.



JPQL이 제공하는 모든 검색 조건 제공

  • member.username.eq("a") : username = 'a'
  • member.username.ne("a") : username ≠ 'a'
  • member.username.eq("a").not() : username ≠ 'a'
  • member.username.isNotNull() : username is not null
  • member.age.in(10,20) : age in (10,20)
  • member.age.notIn(10,20) : age not in(10,20)
  • member.age.between(10,30) : age between 10, 30
  • member.age.goe(30) : age ≥ 30
  • member.age.gt(30) : age > 30
  • member.age.loe(30) : age ≤ 30
  • member.age.lt(30) : age < 30
  • member.username.like("member%") : username like 'member%'
  • member.username.contains("member') : username like '%member%'
  • member.username.startsWith("member") : like 'member%'
  • etc.



결과 조회


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

  • fetchOne() : 단 건 조회

    • 결과가 없으면: null
    • 결과가 둘 이상이면: com.querydsl.core.NonUniqueResultException
  • fetchFirst() : limit(1).fetchOne()과 같다.

  • fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행

  • fetchCount() :count 쿼리로 변경해서 count 수 조회


@Test
public void resultFetchTest() throws Exception {
    
    List<Member> fetch = queryFactory
            .selectFrom(member)
            .fetch();

    Member fetchOne = queryFactory
            .selectFrom(QMember.member)
            .fetchOne();

    Member fetchFirst = queryFactory
            .selectFrom(QMember.member)
            .fetchFirst();

    QueryResults<Member> results = queryFactory
            .selectFrom(member)
            .fetchResults();

    results.getTotal();
    List<Member> content = results.getResults();

    long total = queryFactory
            .selectFrom(member)
            .fetchCount();
    
}



정렬


Querydsl에서는 정렬(Sort)용 메서드 역시 제공된다.
👉 .orderBy(인스턴스명.기준필드.정렬기준.(nullsLast()|nullsFirst()))


/**
 * 회원 정렬 순서
 * 1. 회원 나이 내림차순(desc)
 * 2. 회원 이름 올림차순(asc)
 * 단 2에서 회원 이름이 없으면 마지막에 출력(nulls last)
 */
@Test
public void sort() throws Exception {
  
    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();
}	
  • desc(), asc() :일반 정렬
  • nullsLast(), nullsFirst() :null 데이터 순서 부여



페이징


조회 건수 제한

@Test
public void paging1() throws Exception {
    List<Member> result = queryFactory
            .selectFrom(member)
            .orderBy(member.username.desc())
            .offset(1)
            .limit(2)
            .fetch();

    assertThat(result.size()).isEqualTo(2);
}
  • 페이징 할 때는 orderBy를 넣어서 정렬을 해줘야 잘 동작한다. offset(1).limit(2)는 index(0)을 생략하고 두 개를 선택한다는 뜻

👉 [0][1][2][3]



전체 조회 수

@Test
public void paging2() {
 	QueryResults<Member> queryResults = queryFactory
 		.selectFrom(member)
 		.orderBy(member.username.desc())
 		.offset(1)
 		.limit(2)
 		.fetchResults();
 
 	assertThat(queryResults.getTotal()).isEqualTo(4);
 	assertThat(queryResults.getLimit()).isEqualTo(2);
 	assertThat(queryResults.getOffset()).isEqualTo(1);
 	assertThat(queryResults.getResults().size()).isEqualTo(2);
}

❗ count 쿼리가 실행되니 성능상 주의

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



집합


  • JPQL이 제공하는 모든 집합 함수를 Querydsl에서 제공

집합 함수 코드

@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 사용 → 팀의 이름과 각 팀의 평균 연령을 구해라.

@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) 
            .fetch();
            
    Tuple teamA = result.get(0);
    Tuple teamB = result.get(1);

    assertThat(teamA.get(team.name)).isEqualTo("teamA");
    assertThat(teamA.get(member.age.avg())).isEqualTo(15);

    assertThat(teamB.get(team.name)).isEqualTo("teamB");
    assertThat(teamB.get(member.age.avg())).isEqualTo(35);
}
  • having 함수 역시 같이 사용 가능하다.
    👉 member의 age를 기준으로 그룹핑을 하되 25살 이상만 그룹핑 한다.
...
.groupBy(member.age)
.having(member.age.gt(25))
...
profile
습관이 전부다.

0개의 댓글