[querydsl] 쿼리문

Welcome to Seoyun Dev Log·2023년 4월 25일
0

JPA

목록 보기
4/15

검색 조건

검색조건은 .and(),.or()를메서드체인으로연결할수있다.

📌참고: select , from 을 selectFrom 으로 합칠 수 있음

  @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");
}
  • 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.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%’ 검색

And

and의 경우 연쇄체인이 아닌 , 로 비교하여 사용할 수 있다

  • 연쇄 체이닝
Member findMember = jpaQueryFactory
                .selectFrom(member)
                .from(member)
                .where(member.username.eq("member1")
                        .and(member.age.eq(10))
                )
                .fetchOne();
  • ⭐️ 콤마로 구분
    : 콤마로 구분하는 방식을 선호한다.
    나중에 동적 쿼리를 작성할 때 좋음
Member findMember = jpaQueryFactory
                .selectFrom(member)
                .from(member)
                .where(
                        member.username.eq("member1")
                        ,member.age.eq(10)
                )
                .fetchOne();

결과 조회

  • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne() : 단 건 조회
    • 결과가 없으면 : null
    • 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException 발생
  • fetchFirst() : limit(1).fetchOne()
  • 📌fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
    따라서 쿼리가 두번 실행된다.
  • 📌fetchCount() : count 쿼리로 변경해서 count 수만 조회

⭐️ 페이징은 성능 최적화를 고려해야하는 경우 content 조회와 count 쿼리를 따로 실행해줘야한다.

fetchResults(), fetchCount()의 경우 having절을 사용할 때
다음과 같은 에러 발생으로 querydsl 개발진 측에서 해당 함수를 deprecated 시켰다.
이유는 querydsl 내부에서 count용 쿼리르 만들어서 실행해야 하는데,
이때 작성한 select 쿼리를 기반으로 count를 만들어 낸다. 그런데 이 기능이 select 구문을 단순히 count 처리하는 것으로 바꾸는 정도여서 단순한 쿼리에서는 잘 동작하나 복잡한 쿼리에서는 동작하지 않을 수 있다.
⭐️ 명확하게 카운트 쿼리를 별로도 작성하고, fetch()를 사용해서 해결해야한다.

Caused by: org.hibernate.hql.internal.ast.QuerySyntaxException: unexpected token: having near line 5, column 1

[select count(distinct gcm.memberId)
from com.neibus.admin.fcm.repository.Gcm gcm
  inner join gcm.member as member1
where member1.dropDt is null and gcm.adId is not null and not length(gcm.adId) = 0
having count(gcm.adId) = ?1]
		//리스트 조회
		List<Member> fetch = jpaQueryFactory
                .selectFrom(member)
                .fetch();

        //단건 조회
        Member fetchOne = jpaQueryFactory
                .selectFrom(member)
                .where(member.age.eq(20))
                .fetchOne();

		//처음 한 건 조회
        Member fetchFirst = jpaQueryFactory
                .selectFrom(member)
                //.limit(1).fetchOne() -> fetchFirst()와 동일한 것
                .fetchFirst();

		//페이징
        QueryResults<Member> results = jpaQueryFactory
                .selectFrom(member)
                .fetchResults();

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

정렬

@DisplayName("회원 정렬")
    @Test
    void memberSort() {
        em.persist(new Member(null, 100));
        em.persist(new Member("member5", 100));
        em.persist(new Member("member6", 100));

        List<Member> result = jpaQueryFactory
                .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();

    }
  • jpql 쿼리
/* select member1
from Member member1
where member1.age = 1001
order by member1.age desc, member1.username asc nulls last */

null에 대한 데이터 정렬 순서 부여

nullsLast() , nullsFirst() : null 데이터 순서 부여


페이징

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

@DisplayName("페이징")
    @Test
    void paging1() {
        List<Member> result = jpaQueryFactory
                .selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1)//0부터 시작(0을 스킵한 것)
                .limit(2)
                .fetch();

        assertThat(result.size()).isEqualTo(2);
    }

집합

select를 특정 컬럼으로 지정해주는 경우 querydsl tuple을 조회한다
tuple은 여러개 타입이 복합적으로 있는 경우 조회

실무에서는 tuple 보다 DTO로 바로 조회하는 방법으로 사용한다
JPQL이 제공하는 모든 집합 함수를 제공한다.

@DisplayName("집합")
    @Test
    void aggregation() {
        List<Tuple> result = jpaQueryFactory
                .select(//데이터를 조회하면 tuple을 조회한다
                        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

groupBy 그룹화된 결과를 제한하려면 having절로 제한

@DisplayName("groupBy")
    @Test
    void groupBy() {
        List<Tuple> result = jpaQueryFactory
                .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절 추가
 ...java
    .groupBy(item.price)
    .having(item.price.gt(1000))
    ...

조인

기본 조인

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

  • join() , innerJoin() : 내부 조인(inner join)
  • leftJoin() : left 외부 조인(left outer join)
  • rightJoin() : rigth 외부 조인(rigth outer join)
  • JPQL의 on과 성능 최적화를 위한 fetch 조인 제공
/**
     * 팀 A에 소속된 모든 회원
     */
    @DisplayName("basicJoin")
    @Test
    void basicJoin() {
        List<Member> result = jpaQueryFactory
                .selectFrom(member)
                .join(member.team, team)
                .where(team.name.eq("teamA"))
                .fetch();

        assertThat(result)
                .extracting("username")
                .containsExactly("member1", "member2");
    }

세타 조인

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

모든 회원을 가져오고, 모든 팀을 가져와서 조인을 한다
그런다음 where절로 필터링 하는것
(DB가 성능 최적화를 해준다)

  • from 절에 여러 엔티티를 선택해서 세타 조인
  • 외부조인불가능 다음에설명할조인on을사용하면외부조인가능
    하이버네이트 최신버전이 들어오면서 left join을(외부조인) 할 수 있음
@DisplayName("theta_join")
    @Test
    void theta_join() {
        em.persist(new Member("teamA"));
        em.persist(new Member("teamB"));
        em.persist(new Member("teamC"));

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

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

조인 On절

ON절을 활용한 조인(JPA 2.1 부터 지원)

  • 1.조인 대상 필터링
  • 2.연관관계 없는 엔티티 외부 조인

1) 조인 대상 필터링

on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용하면, where 절에서 필터링 하는 것과 기능이 동일하다.
따라서 on 절을 활용한 조인 대상 필터링을 사용할 때, 내부조인 이면 익숙한 where 절로 해결하고, 정말 외부조인이 필요한 경우에만 이 기능을 사용하자.

  • 외부 조인: null까지 포함
List<Tuple> result = jpaQueryFactory
                .select(member, team)
                .from(member)
                .leftJoin(member.team, team).on(team.name.eq("teamA"))
                .fetch();
  • 내부 조인: 특정 값만 필터링
List<Tuple> result2 = jpaQueryFactory
                .select(member, team)
                .from(member)
                .join(member.team, team)
                .where(team.name.eq("teamA"))
                .fetch();

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

  • 하이버네이트 5.1부터 on을 사용해서 서로 관계가 없는 필드로 외부 조인하는 기능이 추가 되었다.
    물론 내부 조인도 가능하다.
  • 📌주의 ! 문법을 잘 봐야 한다.
    leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어감
    • 일반 조인: leftJoin(member.team, team)
    • on 조인: from(member).leftJoin(team).on(XXX)
  • 연관관계 없는 엔티티 외부 조인
List<Tuple> result = jpaQueryFactory
                .select(member, team)
                .from(member)
                .leftJoin(team).on(member.username.eq(team.name))
                .where(member.username.eq(team.name))
                .fetch();
  • 연관관계가 맺어진 경우
List<Tuple> result = jpaQueryFactory
                .select(member, team)
                .from(member)
                .leftJoin(member.username, team)
                .where(member.username.eq(team.name))
                .fetch();

페치 조인

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

  • join(), leftJoin() 등 조인 기능 뒤에 fetchJoin() 이라고 추가하면 된다.
    참고: 페치 조인에 대한 자세한 내용은 JPA 기본편이나, 활용2편을 참고하자

1) 페치 조인 X

  • 지연로딩으로 Member, Team SQL 쿼리 각각 실행
@PersistenceUnit//엔티티 메니저를 만드는 팩토리
    EntityManagerFactory emf;

    @DisplayName("fetchJoinNo")
    @Test
    void fetchJoinNo() {
        em.flush();
        em.clear();

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

        boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
        assertThat(loaded).as("패치 조인 미적용").isFalse();
    }
/* select member1
from Member member1
where member1.username = 'member1'1 */

2) 페치 조인 O

  • 즉시로딩으로 Member, Team SQL 쿼리 조인으로 한번에 조회
@PersistenceUnit//엔티티 메니저를 만드는 팩토리
EntityManagerFactory emf;
    
@DisplayName("fetchJoin")
    @Test
    void fetchJoin() {
        em.flush();
        em.clear();

        Member findMember = jpaQueryFactory
                .selectFrom(member)
                .join(member.team, team).fetchJoin()
                .where(member.username.eq("member1"))
                .fetchOne();

        boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
        assertThat(loaded).as("패치 조인 미적용").isTrue();
    }
/* select
        member1 
    from
        Member member1   
    inner join
        fetch member1.team as team 
    where
        member1.username = ?1 */

서브 쿼리

쿼리 안에 쿼리를 넣는것
com.querydsl.jpa.JPAExpressions 사용

  • JPAExpressions
    static import 사용 가능
import static com.querydsl.jpa.JPAExpressions.select;

  • ⭐️📌 JPA의 서브쿼리는 From절에서(인라인뷰) 서브쿼리는 지원하지 않는다

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

  • 해결방안
    from 절의 서브쿼리 해결방안
  1. 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
  2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.(성능이 엄청 중요한 상황에서는
  3. nativeSQL을 사용한다. (1번, 2번 시도해보고 안되면 결국 native사용해야함)

🌈from절에서 서브쿼리를 사용하는 이유
DB 쿼리에서 기능을 많이 제공하니까 쿼리를 복잡하게 짜는데
SQL은 데이터를 가지고 오는 것에 집중하고
예를 들면 DATE 날짜 포맷 변경하고 하는 것은 쿼리가 아닌 view 또는 서버에서 처리한다

실시간 트래픽이 중요한 애플리케이션이면 쿼리 하나하나 날리는것이 중요함
그래서 한방 쿼리도 복잡하게 짜는데 오히려 쿼리를 나눠서 짜는것이 효율적일 수 있음

1) where절 서브 쿼리 (eq)

@DisplayName("나이가 가장 많은 회원 조회")
    @Test
    void subQuery() {
        //별칭이 중복되는 경우에는 새로 생성해 줘야 한다
        QMember memberSubQuery = new QMember("memberSubQuery");

        List<Member> result = jpaQueryFactory
                .selectFrom(member)
                .where(member.age.eq(
                        JPAExpressions
                                .select(memberSubQuery.age.max())
                                .from(memberSubQuery)
                ))
                .fetch();

        assertThat(result)
                .extracting("age")
                .containsExactly(40);
    }
select
            member0_.member_id as member_i1_1_,
            member0_.age as age2_1_,
            member0_.team_id as team_id4_1_,
            member0_.username as username3_1_ 
        from
            t_member member0_ 
        where
            member0_.age=(
                select
                    max(member1_.age) 
                from
                    t_member member1_
            )

2) where절 서브쿼리 (goe)

@DisplayName("나이가 평균 이상인 회원 조회")
    @Test
    void subQueryGoe() {
        //별칭이 중복되는 경우에는 새로 생성해 줘야 한다
        QMember memberSubQuery = new QMember("memberSubQuery");

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

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

3) where절 서브쿼리 (in) / (gt)

@DisplayName("나이가 10살 초과인 회원 조회")
    @Test
    void subQueryIn() {
        //별칭이 중복되는 경우에는 새로 생성해 줘야 한다
        QMember memberSubQuery = new QMember("memberSubQuery");

        List<Member> result = jpaQueryFactory
                .selectFrom(member)
                .where(member.age.in(
                        JPAExpressions
                                .select(memberSubQuery.age)
                                .from(memberSubQuery)
                                .where(memberSubQuery.age.gt(10))//초과
                ))
                .fetch();

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

함수

  • null 일때 coalesce
  • eq ===
  • ne !==
  • like 'abc'
  • contains '%abc%'
  • lt <
  • loe <=
  • gt >
  • goe >=

책 추천

profile
하루 일지 보단 행동 고찰 과정에 대한 개발 블로그

0개의 댓글