[Query DSL] 기본 문법

컴공생의 코딩 일기·2023년 2월 6일
0

Query DSL

목록 보기
2/6
post-thumbnail

QueryDSL vs JPQL

@DataJpaTest
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);
        //초기화
        em.flush();
        em.clear();
    }

    @Test
    void startJPQL(){
        // member1을 찾아라
        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
    void startQuerydsl(){

        QMember m = new QMember("m");

        Member findMember = queryFactory.select(m)
                .from(m)
                .where(m.username.eq("member1")) // 파라미터 바인딩 처리
                .fetchOne();
        assertThat(findMember.getUsername()).isEqualTo("member1");
    }
}
  • EntityManagerJPAQueryFactory 생성
  • QueryDSL은 JPQL 빌더

QueryDSL 과 JPQL 차이

  • JPQL

    • 문자로 되어 있다.(실행 시점 오류, 런타임 시점 오류)
    • 파라미터 바인딩(직접)
  • QueryDSL

    • 자바 코드로 되어 있다.(컴파일 시점 오류)
    • 파라미터 바인딩 자동 처리

JPAQueryFactory를 필드로 제공하면 동시성 문제는 어떻게 될까? 동시성 문제는 JPAQueryFactory를 생성할 때 제공하는EntityManager(em)에 달려있다. 스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정하지 않아도 된다.

기본 Q-type 활용

QMember qMember = new QMember("m");
QMember qMember = QMember.member;

검색 조건 쿼리

@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를 메서드 체인으로 연결할 수 있다.
.where(member.username.eq("member1"),
                        (member.age.eq(10)))
  • and는 위에 코드 처럼 사용할 수 있고 파라미터로 넘길 수 있다.
  • 이 경우 null 값을 무시 -> 메서드 추출을 활용해서 동적 쿼리를 깔끔하게 만들 수 있다.

참고: select, fromselectFrom으로 합칠 수 이다.

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

member.username.eq("member1") // username = 'member1'
member.username.ne("member1") //username != 'member1'
member.username.eq("member1").not() // username != 'member1'
member.username.isNotNull() //이름이 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) //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%") //like 검색
member.username.contains("member") // like ‘%member%’ 검색
member.username.startsWith("member") //like ‘member%’ 검색
...

결과 조회

  • fetch(): 리스트 조회, 데이터가 없으면 빈 리스트를 반환
  • fetchOne(): 단 건 조회
    • 결과가 없으면: null
    • 결과가 둘 이상이면: com.querydsl.core.NonUniqueResultException
  • fetchFirst(): limit(1).fetchOne()
//List
List<Member> fetch = queryFactory
 .selectFrom(member)
 .fetch();
//단 건
Member findMember1 = queryFactory
 .selectFrom(member)
 .fetchOne();
//처음 한 건 조회
Member findMember2 = queryFactory
 .selectFrom(member)
 .fetchFirst();

참고: fetchResults(), fetchCount()Querydsl 5.0.0 부터 deprecated 되었다. fetchResults(), fetchCount()는 둘 다 Querydsl 내부에서 count 쿼리를 만들어서 실행해야 하는데, 이때 작성한 select 쿼리를 기반으로 count 쿼리를 만들어낸다. 그런데 이 기능이 select 구문을 단순히 count 처리하는 것으로 바꾸는 정도여서, 단순히 쿼리에서는 잘 동작하지만, 복잡한 쿼리에서는 잘 동작하지 않는다. 그렇기 때문에 count 쿼리를 별도로 작성하고, fetch()를 사용해서 해결해야 한다.

정렬과 페이징

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

페이징

@Test
public void paging1() {
 List<Member> result = queryFactory
 .selectFrom(member)
 .orderBy(member.username.desc())
 .offset(1) //0부터 시작(zero index)
 .limit(2) //최대 2건 조회
 .fetch();
 assertThat(result.size()).isEqualTo(2);
}

집합

집합 함수

/**
 * JPQL
 * select
 * COUNT(m), //회원수
 * SUM(m.age), //나이 합
 * AVG(m.age), //평균 나이
 * MAX(m.age), //최대 나이
 * MIN(m.age) //최소 나이
 * from Member m
 */
@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);
}
  • JPQL이 제공하는 모든 집합 함수를 제공
  • Tuple 객체로 값을 반환

GroupBy 사용

 /**
 * 팀의 이름과 각 팀의 평균 연령을 구해라.
 */
 List<Tuple> result = queryFactory
 .select(team.name, member.age.avg())
 .from(member)
 .join(member.team, team)
 .groupBy(team.name)
 .fetch();

groupBy(), having() 사용

......
.groupBy(item.price)
.having(item.price.gt(1000))
......

Join

기본 Join

조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q타입을 지정하면 된다.

/**
 * 팀 A에 소속된 모든 회원
 */
@Test
public void join() throws Exception {
 QMember member = QMember.member;
 QTeam team = QTeam.team;
 List<Member> result = queryFactory
 .selectFrom(member)
 .join(member.team, team)
 .where(team.name.eq("teamA"))
 .fetch();
 assertThat(result)
 .extracting("username")
 .containsExactly("member1", "member2");
}
  • join(), innerJoin() : 내부 조인(inner join)
  • leftJoin() : left 외부 조인(left outer join)
  • rightJoin() : right 외부 조인(right outer join)

세타 조인

연관관계 없는 필드로 조인

/**
 * 세타 조인(연관관계가 없는 필드로 조인)
 * 회원의 이름이 팀 이름과 같은 회원 조회
 */
@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");
}
  • from 절에 여러 엔티티를 선택해서 세타 조인
  • 외부 조인 불가능 -> 단 on을 사용하면 외부 조인 가능

Join - on절

  • on절을 활용한 조인(JPA 2.1 부터 지원)
    • 조인 대상 필터링
    • 연관관계 없는 엔티티 외부 조인

조인 대상 필터링

/**
 * 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
 * JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'teamA'
 * SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and 
t.name='teamA'
 */
@Test
public void join_on_filtering() throws Exception {
 List<Tuple> result = queryFactory
 .select(member, team)
 .from(member)
 .leftJoin(member.team, team).on(team.name.eq("teamA"))
 .fetch();
 for (Tuple tuple : result) {
 System.out.println("tuple = " + tuple);
 }
}

결과

t=[Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
t=[Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
t=[Member(id=5, username=member3, age=30), null]
t=[Member(id=6, username=member4, age=40), null]
  • on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용할 경우 where 절에서 필터링 하는 것과 기능이 동일하다.
  • on 절을 활용한 조인 대상 필터링을 사용할 때 내부조인 이면 익숙한 where 절로 해결하고 , 외부조인이 필요할 경우 on절을 사용하자

연관관계 없는 엔티티 외부 조인

/**
 * 2. 연관관계 없는 엔티티 외부 조인
 * 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
 * JPQL: SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
 * SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name
 */
@Test
public void join_on_no_relation() throws Exception {
 em.persist(new Member("teamA"));
 em.persist(new Member("teamB"));
 List<Tuple> result = queryFactory
 .select(member, team)
 .from(member)
 .leftJoin(team).on(member.username.eq(team.name))
 .fetch();
 for (Tuple tuple : result) {
 System.out.println("t=" + tuple);
 }
}

결과

t=[Member(id=3, username=member1, age=10), null]
t=[Member(id=4, username=member2, age=20), null]
t=[Member(id=5, username=member3, age=30), null]
t=[Member(id=6, username=member4, age=40), null]
t=[Member(id=7, username=teamA, age=0), Team(id=1, name=teamA)]
t=[Member(id=8, username=teamB, age=0), Team(id=2, name=teamB)]
  • 하이버네이트 5.1부터 on을 사용해서 서로 관계가 없는 필드로 외부 조인하는 기능이 추가되었다.(내부조인도 가능)
  • 주의! leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다.
    • 일반조인 : : leftJoin(member.team, team) -> member.id = team.id 검색
    • on조인 : from(member).leftJoin(team).on(xxx) -> on절 조건으로 검색

페치조인

즉시로딩으로 Member, Team SQL 쿼리 조인으로 한번에 조회

@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(), leftJoin()등 조인 기능 뒤에 fetchJoin()을 추가하면 된다.

서브쿼리

  • com.querydsl.jpa.JPAExpressions 사용

where 절에서 서브쿼리 사용

/**
 * 나이가 가장 많은 회원 조회
 */
@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);
}

select 절에서 서브쿼리 사용

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)));
}

from 절의 서브쿼리 한계 JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다. 당연히 Querydsl도 지원하지 않는다. 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다. Querydsl도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.

from 절의 서브쿼리 해결방안

  1. 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
  2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
  3. nativeSQL을 사용한다.

Case 문

select, 조건절(where), order by에서 사용 가능

단순 조건

List<String> result = queryFactory
 .select(member.age
 .when(10).then("열살")
 .when(20).then("스무살")
 .otherwise("기타"))
 .from(member)
 .fetch();

복잡한 조건

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();

NumberExpression 객체를 사용한 임의의 순서 정하기

// 1. 0 ~ 30살이 아닌 회원을 가장 먼저 출력
// 2. 0 ~ 20살 회원 출력
// 3. 21 ~ 30살 회원 출력

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> result = queryFactory
 .select(member.username, member.age, rankPath)
 .from(member)
 .orderBy(rankPath.desc())
 .fetch();
for (Tuple tuple : result) {
 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);
}
결과
username = member4 age = 40 rank = 3
username = member1 age = 10 rank = 2
username = member2 age = 20 rank = 2
username = member3 age = 30 rank = 1

상수, 문자 더하기

상수가 필요하면 Expressions.constant(xxx) 사용

Tuple result = queryFactory
 .select(member.username, Expressions.constant("A"))
 .from(member)
 .fetchFirst();

문자 더하기 concat

String result = queryFactory
 .select(member.username.concat("_").concat(member.age.stringValue()))
 .from(member)
 .where(member.username.eq("member1"))
 .fetchOne();
  • stringValue() : 문자열 타입으로 변환
profile
더 좋은 개발자가 되기위한 과정

0개의 댓글