ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • QueryDSL 기본 문법
    이커머스 devops/국비교육 2025. 11. 19. 11:08

    QueryDSL 의존성 추가 

    // build.gradle
    dependencies {
       // 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"
    }
    
    tasks.named('compileJava') {
        options.annotationProcessorPath = configurations.annotationProcessor
    }

     

     

    검증용 QClass 확인

     

     

    - gradle > tasks > other > compileJava (임의지정이름) 클릭 > QClass 생성

     

     

    cf. QClass는 git에 올리면 안 된다 

    QClass는 정적 파일이 아니라 빌드 시점에 자동 생성되는 산출물로 버전관리 대상이 아니다

    환경 차이로 인해 충돌이 많이 발생하기 때문에 build 폴더에 생성하고 build 폴더는 .gitignore

     

     

    쿼리 파라미터 로그 남기기

    // build.gradle
    dependencies { 
     implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
    }

     

     

    엔티티 작성 - Member, Team 

    @Entity
    @Getter
    @Setter // 실무에서는 사용 지양
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    // 기본 생성자는 JPA 스펙상 PROTECTED로 열어둬야 함
    @ToString(of = {"id", "username", "age"})
    public class Member {
        @Id @GeneratedValue
        @Column(name = "member_id")
        private Long id;
    
        private String username;
    
        private int age;
    
        // 연관관계 주인
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "team_id")
        private Team team;
    
        public Member(String username) {
            this(username, 0);
        }
    
        public Member(String username, int age) {
            this(username, age, null);
        }
    
        public Member(String username, int age, Team team) {
            this.username = username;
            this.age = age;
            if (team != null) {
                changeTeam(team);
            }
        }
    
    	// 연관관계 편의 메서드
        public void changeTeam(Team team) {
            // 양방향 셋팅
            this.team = team;
            team.getMembers().add(this);
        }
    }
    @Entity
    @Getter
    @Setter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @ToString(of = {"id", "name"})
    public class Team {
        @Id
        @GeneratedValue
        private Long id;
    
        private String name;
    
        @OneToMany(mappedBy = "team")
        private List<Member> members = new ArrayList<>();
    
        public Team(String name) {
            this.name = name;
        }
    }

     

     

    JPQL vs Querydsl

    @Test
    public void startJPQL() {
        Member findByJPQL = em.createQuery("select m from Member m where m.username = :username", Member.class)
                .setParameter("username", "memberA").getSingleResult();
    
        Assertions.assertThat(findByJPQL.getUsername()).isEqualTo("memberA");
    }
    
    @Test
    public void startQuerydsl() {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        QMember m = new QMember("m");
    
        Member indByQuerydsl = queryFactory
                .select(m)
                .from(m)
                .where(m.username.eq("memberA"))
                .fetchOne();
    
        Assertions.assertThat(indByQuerydsl.getUsername()).isEqualTo("memberA");
    }
    • JPLQ : 문자(실행 시점 오류), 파라미터 바인딩 직접
    • Querydsl : 코드(컴파일 시점 오류), 파라미터 바인딩 자동 처리
    • cf. 스프링프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager를 접근해도 트랜잭션마다 별도의 영속성 컨텍스트를 제공하기 때문에 동시성 문제는 고려하지 않아도 된다 

     

     

    기본 Q-Type 활용

    - Q클래스 인스턴스 사용 2가지 방법

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

     

     

    - 실행되는 JPQL 확인

     // application.yml
     spring.jpa.properties.hibernate.use_sql_comments: true

     

     

    - 기본 검색 조건 쿼리

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

     

     

    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%’ 검색

     

     

    AND 조건을 파라미터로 처리

    @Test
    public void searchAndParam() {
        List<Member> result1 = queryFactory
                    .selectFrom(member)
                    .where(member.username.eq("member1"), member.age.eq(10))
                .fetch();
        assertThat(result1.size()).isEqualTo(1);
    }

     

     

    결과 조회

    @Test
    public void resultFetch() {
        List<Member> fetch = queryFactory.selectFrom(member).fetch();
    
        Member fetchOne = queryFactory.selectFrom(member).fetchOne();
    
        Member fetchFirst = queryFactory.selectFrom(member).fetchFirst();
    
        // 페이징에서 사용
        QueryResults<Member> results = queryFactory.selectFrom(member).fetchResults();
        results.getTotal();
        List<Member> content = results.getResults();
    
        // count 쿼리로 변경
        long count = queryFactory
                .selectFrom(member)
                .fetchCount();
    }

     

     

    정렬

    /*
    * 1. 회원 나이 내림차순
    * 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(memberNull.getUsername()).isNull();
    }

     

     

    페이징

    // 조회 건수 제한
    @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);
    }
    
    // 전체 조회 수가 필요
    @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 쿼리가 실행되니 성능상 주의

     

     

    집합

    /**
     * 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);
    }
    • 데이터 타입이 여러 개 > tuple 사용, 실무에서는 거의 사용하지 않음 

     

     

    GroupBy 사용

    /**
    * 팀의 이름과 각 팀의 평균 연령을 구해라.
    */
    @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);
    }

     

     

    조인

    기본 조인

    - 첫 번째 파라미터에 조인 대상을 지정하고 두 번째 파라미터에 alias로 사용할 Q 타입을 지정한다

    - join(조인대상, 별칭으로 사용할 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() : 내부 조인
    • leftJoin() : left 외부 조인
    • rightJoin() : right 외부 조인 
    • JPQL의 on과 성능 최적화를 위한 fetch 조인 제공

     

     

    세타 조인

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

    /**
    * 세타 조인(연관관계가 없는 필드로 조인)
    * 회원의 이름이 팀 이름과 같은 회원 조회
    */
    @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 사용하면 외부 조인 가능

     

     

    on 절

    - 조인 대상 필터링

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

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

    • 내부조인을 사용하면 where절에서 필터링 하는 것과 동일하다 

     

     

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

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

    • leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다
      • 일반 조인 : leftJoin(member.team, team)
      • on 조인 : from(member).leftJoin(team).on(...)

     

     

    페치 조인

    - SQL 조인을 활용해서 연관된 엔티티를 SQL 한 번에 조회하는 기능

    - 주로 성능 최적에 사용되는 방법

    // 페치 조인 미적용 : 지연로딩으로 Member, Team SQL 쿼리 각각 실행
    @PersistenceUnit
    EntityManagerFactory emf;
    
    @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();
    }
    
    // 페치 조인 적용 : 즉시로딩으로 쿼리 한 번에 조회
    @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();
    }

     

     

     서브 쿼리

    - com.querydsl.jpa.JPAExpressions

    /**
     * 서브 쿼리 eq 사용
     * - 나이가 가장 많은 회원 조회
    */
    @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);
    }
    
    /**
     * 서브 쿼리 goe 사용
     * - 나이가 평균 나이 이상인 회원
    */
    @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);
    }
    
    /**
     * 서브쿼리 여러 건 처리 in 사용
    */
    @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);
    }
    
    
    /**
     * select 절에 subquery
    */
    public void selectSubQuery() {
        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)));
        }
    }
    }
    • from 절의 서브쿼리 한계 - JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다
      • 해결방안 3가지 
      • 서브쿼리를 join으로 변경한다 
      • 애플리케이션에서 쿼리를 분리해 2번 실행한다
      • 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();

     

     

    - order by + case 문

    /*
    * 예를 들어서 다음과 같은 임의의 순서로 회원을 출력하고 싶다면?
    * 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);
    }
    • Querydsl은 자바 코드로 작성하기 때문에 rankPath처럼 복잡한 조건을 변수로 선언해서  select절, orderBy절에서 함께 사용할 수 있다

     

     

    상수, 문자 더하기

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

    - 문자 더하기 concat 사용

    Tuple result = queryFactory
            .select(member.username, Expressions.constant("A"))
            .from(member)
            .fetchFirst();
            
    String result = queryFactory
            .select(member.username.concat("_").concat(member.age.stringValue()))
            .from(member)
            .where(member.username.eq("member1"))
            .fetchOne();
    728x90
Designed by Tistory.