본문 바로가기

Back-End/QueryDSL

QueryDsl 기본 문법

1. 엔티티 설계(단반향 매핑)

Member

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
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 void changeTeam(Team team) {
    	this.team = team;
    }
}

Team

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "team_id")
    private Long id;

    private String name; // 팀 이름
}

2. 데이터 세팅(샘플 데이터 삽입)

@Component
public class InitData {

    private final EntityManager em;

    public InitData(EntityManager em) {
        this.em = em;
    }

    @PostConstruct
    @Transactional
    public void init() {
        Team teamA = Team.builder().name("TeamA").build();
        Team teamB = Team.builder().name("TeamB").build();

        em.persist(teamA);
        em.persist(teamB);

        Member member1 = Member.builder().username("John").age(25).team(teamA).build();
        Member member2 = Member.builder().username("Jane").age(30).team(teamA).build();
        Member member3 = Member.builder().username("Tom").age(35).team(teamB).build();
        Member member4 = Member.builder().username("Kim").age(40).team(teamB).build();

        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);
    }
}

3. QueryDSL 예제

단방향 매핑에서는 Team이 Member로 참조하지 않으므로, 모든 쿼리는 Member를 중심으로 작성

1) 기본 Q-Type 활용

Q클래스 인스턴스를 사용하는 2가지 방법

QMember qMember = new QMember('m'); // 별칭 직접 지정
QMember qMember = QMember.member; // 기본 인스턴스 사용
"QMember.member에서 QMember는 static import가 가능하여 member만 써도됨"
QMember member = QMember.member;
List<Member> members = queryFactory
            .selectFrom(member)
            .fetch(); // 모든 멤버 조회
members.forEach(m -> System.out.println("Member: " + m.getUsername()));

2) 검색 조건 쿼리

List<Member> members = queryFactory
        .selectFrom(member)
        .where(
            member.age.gt(30), // 나이가 30보다 큰 멤버 (gt는 grater 줄임말)
            member.team.name.eq("TeamB") // 팀 이름이 TeamB인 조건(eq는 equals 줄임말)
        )
        .fetch();

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

2-1) AND 조건을 파라미터로 처리

public void searchAndParam() {
	List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.username.eq("member1"),
                   member.age.eq(10))
            .fetch();
}
  • where() 에 파라미터로 검색조건을 추가하면 AND 조건이 추가됨.

3) 결과 조회

// List(전체 조회)
List<Member> fetch = queryFactory
        .selectFrom(member)
        .fetch();
        
// 단건 조회
Member singleMember = queryFactory
        .selectFrom(member)
        .where(member.username.eq("John")) // 이름이 Jhon인 조건
        .fetchOne();
// 처음 한 건 조회
Member findFirstMember = queryFactory
        .selectFrom(member)
        .fetchFirst();
        
// 총 멤버 수 조회        
long count = queryFactory
        .selectFrom(member)
        .fetchCount();
        
// 페이징에서 사용
QueryResults<Member> results = queryFactory
        .selectFrom(member)
        .fetchResults();

4) 정렬

List<Member> sortedMembers = queryFactory
         .selectFrom(member)
         .orderBy(
            member.age.desc(), // 나이 내림차순
            member.username.asc() // 이름 오름차순
            member.username.asc().nullsLast() // 이름 오름차순인데 null이면 마지막
         )
         .fetch();

5) 페이징

List<Member> pagedMembers = queryFactory
         .selectFrom(member)
         .orderBy(member.age.desc())
         .offset(0) // 0번 페이지부터
         .limit(2) // 2개 가져오기
         .fetch();
  • 주의! 이렇게 자동화된 count 쿼리는 원본 쿼리와 같이 모두 조인 해버리기 떄문에 성능이 안나올 수 있다. count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면, count 전용 쿼리를 별도로 작성해야 한다.(그래서 fetch()로 List 형태로 가져와 count 쿼리와 조합해 페이징을 처리하자!)

6. 집합

List<Tuple> result = queryFactory
       .select(member.team.name, member.age.avg())
       .from(member)
       .groupBy(member.team.name) // 팀별로 그룹화 // having절로 그룹화된 결과를 제한 가능
       .fetch();
       
result.forEach(tuple -> {
    System.out.println("Team: " + tuple.get(member.team.name)
            + ", Avg Age: " + tuple.get(member.age.avg()));
});

//전체 조회 수
queryResults<Member> queryResults = queryFactory
        .selectFrom(member)
        .orderBy(member.username.desc())
        .offset(1)
        .limit(2)
        .fetchResults();

7. 조인 - 기본 조인

List<Member> teamAMembers = queryFactory
        .selectFrom(member)
        .join(member.team, team) // Member와 Team 조인, team은 QTeam team임
        .where(team.name.eq("TeamA"))
        .fetch();

8. 조인 - on절

8-1) 조인 대상 필터링
ex) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회

List<Tuple> result = queryFactor
        .select(member, team) // Member 엔티티의 데이터와 Team 엔티티의 데이터를 함께 조회
        .from(member) // 데이터 소스(Member 테이블)
        .leftJoin(member.team, team).on(team.name.eq("teamA")) // 조건 기반 조인
        .fetch();
        
"
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]       
"

9. 조인 - 페치 조인

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

10. orderBy에서 Case문과 함께 사용하기 예제
ex)
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, 31)).then(1)
      .otherwise(3);

List<Tuple> result = queryFactory
        .select(member.username, member.age, rankPath)
        .from(member)
        .orderBy(rankPath.desc())
        .fetch();
"        
username = member4 age = 40 rank = 3
username = member1 age = 10 rank = 2
username = member2 age = 20 rank = 2
username = member3 age = 30 rank = 1       
"

QueryDSL은 자바 코드로 작성하기 때문에 rankPath처럼 복잡한 조건을 변수로 선언해서 select절, orderBy절에서 함께 사용할 수 있다.

11. 문자 더하기

Strint result = queryFactory
         .select(member.username.concat("_").concat(member.age.stringValue()))
         .from(member)
         .where(member.username.eq("member1"))
         .fetchOne();
"
member1_10
"
  • member.age.stringValue() 부분이 중요하다. 문자가 아닌 다른 타입들은 stringValue()로 문자로 변환할 수 있다. 이 방법은 ENUM을 처리할 때 자주 사용한다.

'Back-End > QueryDSL' 카테고리의 다른 글

Spring Data Jpa와 QueryDSL  (0) 2025.01.17
QueryDSL 중급 문법  (0) 2025.01.17
JPQL을 QueryDSL로 바꿔보자!  (0) 2025.01.17
QueryDsl 3.x 설정  (0) 2024.12.29