실전! Querydsl : 기본 문법

jkky98·2024년 10월 23일
0

Spring

목록 보기
60/77

환경설정(spring boot 3)

	//Querydsl 추가
	implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
	annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
	annotationProcessor "jakarta.annotation:jakarta.annotation-api"
	annotationProcessor "jakarta.persistence:jakarta.persistence-api"

boot2에 비해 매우 간편해졌는데, 위의 정보만 build.gradle의 dependencies에 추가해주고 빌드만 다시 해주면 Q타입 생성이 자동적으로 잘 이루어진다.

Q-Type

querydsl 문법 사용을 위해 엔티티 객체가 아닌 엔티티와 매핑되어 자동으로 생성된 Q-Type의 객체를 가져와야 한다.

사용할 클래스에 다음과 같이 선언해서 사용하면 된다.

QMember qMember = new QMember("m"); //별칭 직접 지정 
QMember qMember = QMember.member; //기본 인스턴스 사용(이 방법을 많이 사용 + static import)

이제부터는 각각의 기능들을 모두 코딩하며 해석해보도록 하겠다. querydsl은 직관적이며 sql을 모른다면 sql 문법과 함께 공부하기도 좋다. 내가 고안한 공부 방식은 두 가지인데 검색 조건에 대해 querydsl로 작성하는 법과 작성된 querydsl을 해석하는 것이다.

작성된 querydsl을 해석하는 것은 db에서 동작하는 순서를 인지하면서 따라가는 것이 포인트다. 해당 querydsl -> 나타난 결과만 보는 것은 전혀 도움이 되지 않으니까.

이 포스팅으로 공부를 한다면 먼저 코드만 보고 해석(순서까지)을 먼저 시도하고, 이를 한 문장으로 적어놓고 모든 코드에 대한 해석이 끝났다면 한 문장으로 적은 쿼리요청문으로 하여금 다시 querydsl문을 작성하는 것이다.

sql 쿼리의 실행 순서

  1. FROM: 데이터 소스 테이블을 결정합니다.
  2. JOIN: 필요할 경우, 다른 테이블과 결합(JOIN)합니다.
  3. WHERE: 필터 조건을 적용하여 데이터를 필터링합니다.
  4. GROUP BY: 데이터를 그룹화합니다(필요한 경우).
  5. HAVING: 그룹화된 데이터에 필터 조건을 적용합니다.
  6. SELECT: 필요한 컬럼을 선택합니다.
  7. ORDER BY: 데이터를 정렬합니다.

기본 검색

@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");

    }

selectfrom(member) = select(member).from(member) 이므로 member를 전체 조회하겠다는 것이다.

  1. from member : member 테이블 전체 조회
  2. where(member.username.eq("member1").and(member.age.eq(10))) : member의 username이 "member1"이면서 age가 10인 경우 조건 걸기
  3. select member: 2까지 수행되었을 때 해당하는 member 엔티티 가져오기
  4. fetchOne() : 결과 하나 가져오기

정렬

@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()).isEqualTo(null);

    }
  1. from member : member 테이블 전체 조회
  2. where(member.age.eq(100)) : member 테이블에서 age가 100인 것만 필터링
  3. select member위의 조건으로 member엔티티 모두 조회
  4. orderBy(member.age.desc(), member.username.asc().nullsLast()) : 필터링된 것들을 1차적으로 age 내림차순 정렬, age가 같다면 username을 오름차순으로, age가 null이 존재한다면 마지막으로 빼기

paging

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

        assertThat(result.size()).isEqualTo(2);
    }
  1. from member : member 테이블 전체 조회
  2. offset(1), limit(2) : 위의 결과에서 첫 행 빼고 두번째부터 총 2개 가져오기
  3. select member : 1~3 결과에 해당하는 member(2개) select
  4. orderBy(member.username.desc()): member.username 내림차순으로 정렬

groupBy

	@Test
    void group() {
        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);

    }
  1. from member : member 테이블 전체 조회
  2. .join(member.team, team) : member와 연관관계인 team에 대해 join
  3. .groupBy(team.name): team.name으로 group화 됨.
  4. .select(team.name, member.age.avg()): avg()가 groupBy로 인한 그룹화를 인지하여 avg()를 계산함.

Join

@Test
    public void join() {
        List<Member> result = queryFactory
                .selectFrom(member)
                .join(member.team, team)
                .where(team.name.eq("teamA"))
                .fetch();

        assertThat(result)
                .extracting("username")
                .containsExactly("member1", "member2");
    }
  1. from member : member 테이블 전체 조회
  2. .join(member.team, team) : member와 연관관계인 team에 대해 join
  3. .where(team.name.eq("teamA")) : join후 결과에 대해 username이 teamA인 경우만 필터링
  4. select(member): 최종 결과 member 엔티티로 select

Left outer join

@Test
    public void join_on_filtering() {
        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);
        }
    }
  1. from member : member 테이블 전체 조회
  2. .leftJoin(member.team, team).on(team.name.eq("teamA")) : member와 연관관계인 team에 name이 teamA인 것들만 left join (member는 줄지 않음. teamA에 해당하지 않는 member의 join 대상 필드에는 null 채워짐)
  3. select(member): 최종 결과 member 엔티티, team 엔티티로 추출

fetchJoin

	@Test
    public void fetchJoinUse() {
        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();
    }
  1. from member : member 테이블 전체 조회
  2. .join(member.team, team) : member와 연관관계인 team에 대해 fetch join (일반 join시에는 DB에서 join은 되지만 member만 가져올 경우 member.team시에 또 다시 team select 쿼리가 나가게 됨)
  3. .where(member.username.eq("member1")) : join후 member.username이 member1인 것들로 필터링
  4. select member : 필터된 member 엔티티만 모두 가져옴.

SubQuery

where절 subQuery

@Test
    public void subQueryAvgOver() {

        QMember memberAvg = new QMember("memberAvg");

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

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


    }
  1. from member : member 테이블 전체 조회
  2. .where(member.age.goe( select(memberAvg.age.avg()) .from(memberAvg) )) : where절 안에서 subQuery 동작, Member테이블의 age.avg()를 구함
    avg값 이상인 member.age에 해당하는 것들로 필터링
  3. select member : 필터된 member 엔티티만 모두 가져옴.

select절 subQuery

@Test
    public void selectSubQuery() {
        QMember memberSub = new QMember("memberSub");

        List<Tuple> result = queryFactory
                .select(member.username,
                        select(memberSub.age.avg())
                                .from(memberSub)
                )
                .from(member)
                .fetch();

        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
        }
    }
  1. from member : member 테이블 전체 조회
  2. .select(member.username, select(memberSub.age.avg()) .from(memberSub) ) : member의 username과 member.age.avg()를 select (반환 튜플에 대해avg는 모두 동일함)

from절 subQuery

불가능

subQuery사용 주의

from절은 subQuery가 안되는 등 불편함이 존재하고 최적화 관점에서도 오히려 subquery를 사용하지 않고 query를 두 번으로 나누거나 어플리케이션 내에서 필터링을 할 수 있을 것이다. "하나의 쿼리"와 같은 극한의 쿼리 성능 최적화는 복잡성을 최적화 정도에 비해 너무 많이 높인다. 대용량 트래픽에 대비하기 위해 편리함을 포기하면서까지 쿼리문 성능 최적화의 극단까지 추구할 필요가 없는 것이 실제로 이 과정이 사용성을 크게 개선하기보다는 개발자들의 유지 보수 관리 환경을 더 나쁘게 만드는 측면이 강화되므로 너무 목숨걸지 말자.

profile
자바집사의 거북이 수련법

0개의 댓글