-
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'이커머스 devops > 국비교육' 카테고리의 다른 글
동적 쿼리와 성능 최적화 조회 (0) 2025.11.19 QueryDSL 중급 문법 (0) 2025.11.19 영속성 컨텍스트와 트랜잭션 & QueryDSL (0) 2025.11.16 RESTful API 설계하기 (0) 2025.11.16 공통 응답 처리와 예외 핸들링 (0) 2025.11.16