QueryDSL 시작

이동건 (불꽃냥펀치)·2025년 3월 15일

예제 도메인 모델

Member

@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of={"id","username","age"})
public class Member {
    @Id
    @GeneratedValue
    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);
    }
}
  • ToString은 가급적이면 연관관계에 해당하지 않는 걸로 설정
  • NoArgsConstructor => protected로 외부에서 접근 못하게 막음
  • changeTeam으로 양방향 연관관계 한번에 처리하기

Team

@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class Team {
    @Id
    @GeneratedValue
    @Column(name = "team_id")
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members=new ArrayList<>();

    public Team(String name) {
        this.name = name;
    }
}
  • Member와 Team의 양방향 연관관계 설정
  • 하지만 연관관계의 주인은 Member이므로 Team에서는 조회만 가능함



기본 문법

JPQL

@Test
    void startJpql(){
        String qlString= "select m from Member m " +
                "where m.username = :username";
        Member findMember = (Member)em.createQuery(qlString).setParameter("username","member1").getSingleResult();
        assertThat(findMember.getUsername()).isEqualTo("member1");
    }
  • 사전에 저장된 회원 값 찾기

QueryDSL

  @Test
    void startQuerydls(){

        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        QMember m= new QMember("m");
        Member findMember = queryFactory.select(m)
                .from(m)
                .where(m.username.eq("member1"))
                .fetchOne();
        assertThat(findMember.getUsername()).isEqualTo("member1");

    }
  • EntityManagerJPAQueryFactory 생성
@BeforeEach
    public void before() {
        queryFactory = new JPAQueryFactory(em);
//...
}
  • 쿼리팩토리를 필드로 제공하면 EntityManager가 크랜잭션 마다 별도의 영속성 컨텍스트를 제공하므로 동시성 문제는 걱정하지 않아도 됨

기본 Q-Type 활용

QMember qMember= new QMember("m");
QMember qMember = QMember.member 
  • 기본 인스턴스를 static import와 함께 사용 가능
  • 같은 테이블을 조인해야 하는 경우가 아니라면 기본 인스턴스를 권장한다

검색조건 쿼리

@Test
    void search(){
        Member findMember= queryFactory.select(member).from(member).where(member.username.eq("member1").and(member.age.eq(10))).fetchOne();
        assertThat(findMember.getUsername()).isEqualTo("member1");
    }
  • 검색조건은 and,or를 메서드 체인으로 연결이 가능
  • select,fromselect from으로 합치기도 가능

검색조건 예시

  • eq(member1): =
  • ne(member1): !=
  • eq(member1).not(): !=
  • isNotNull(): is not null
  • in(a,b)
  • notIn(a,b)
  • between(a,b)
  • goe(10) : >=
  • gt(10): >
  • loe(10): <=
  • lt(10): <
  • like(member1%)
  • contains(member) : like %member1%
  • startsWith(member): like member%
 @Test
    void searchAndParam(){
        Member findMember = queryFactory.selectFrom(member).where(member.username.eq("member1"),member.age.eq(10)).fetchOne();

        assertThat(findMember.getUsername()).isEqualTo("member1");
    }
  • where()에 파라미터로 검색조건을 추가하면 And 조건이 추가됨
  • 이 경우 null값은 무시 => 메서드 추출을 활용해서 깔끔한 동적 쿼리를 만들 수 있다

결과조회

  • fetch(): 리스트 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne(): 단건 조회
    • 결과가 없으면 null
    • 결과가 둘 이상이면 예외 반환
  • fetchFirst(): limit(1).fetchOne()
  • fetchResults(): 페이징 정보 포함, total count 쿼리 추가 실행
  • fetchCount(): count 쿼리로 변경해서 count 수 조회

정렬

@Test
    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();
        System.out.println(result.toString());
        Member member5 = result.get(0);
        Member member6 = result.get(1);
        Member member7 = result.get(2);
        assertThat(member5.getUsername()).isEqualTo("member5");
        assertThat(member6.getUsername()).isEqualTo("member6");
        assertThat(member7.getUsername()).isNull();
    }
  • desc(),asc(): 일반 정렬
  • nullsLast(),nullsFirst(): null 데이터 순서 부여

페이징

조회 건수 제한

 @Test
    void paging() {

        List<Member> result = queryFactory.selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1)
                .limit(2)
                .fetch();
    }
  • offset(1): 0부터 조회
  • limit(2): 최대 2건 조회

전체 조회 수

 @Test
    void paging2(){
        QueryResults<Member> result = queryFactory.selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1)
                .limit(2)
                .fetchResults();
        assertThat(result.getTotal()).isEqualTo(4);
        assertThat(result.getLimit()).isEqualTo(2);
        assertThat(result.getOffset()).isEqualTo(1);

    }

집합

집합 함수

  • COUNT(m) : 회원 수
  • SUM(m.age): 나이 합
  • AVG(m.age): 평균 나이
  • MAX(m.age): 최대 나이
  • MIN(m.age): 최소 나이

사용 예시

@Test
    void aggregation(){
        List<Tuple> fetch = queryFactory.select(member.count(), member.age.sum(), member.age.avg(), member.age.max(), member.age.min()).from(member).fetch();
        Tuple tuple = fetch.get(0);
        assertThat(tuple.get(member.count())).isEqualTo(4) ;
        assertThat(tuple.get(member.age.sum())).isEqualTo(100);
    }

group by

 @Test
    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);
    }
  • 그룹화의 결과를 제한하려면 Having 사용

조인 - 기본 조인

join(조인대상, 별칭으로 사용할 Q 타입)

기본조인

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

        assertThat(result).extracting("username").containsExactly("member1","member2");
    }
  • join(),innerJoin(): 내부조인
  • leftJoin: left 외부 조인
  • rightJoin(); right 외부 조인

세타조인

 @Test
    void setterJoin(){
        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 절

@Test
    void join_on_filterring(){
        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);
        }
    }
  • 회원은 모두 조회하면서 팀 이름이 teamA인 팀만 조인

연관관계 없는 조인

 @Test
    void join_on_no_realation(){
        em.persist(new Member("teamA"));
        em.persist(new Member("teamB"));
        List<Tuple> result = queryFactory.select(member,team).from(member)
                .leftJoin(member.team,team).on(member.username.eq("teamA")).fetch();

        for(Tuple tuple : result){
            System.out.println("tuple : "+tuple);
        }
    }
  • 주의: 문법을 잘 봐야 한다 leftJoin() 부분에 일반 조인과 다르게 엔터티 하나만 들어감

조인 - 페치조인

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

    }
  • 일반조인을 사용하면 사용하지 않는 엔터티인 Team까지 조회하지 않음
  • 페치 조인을 사용하면 Team까지 한꺼번에 가져옴

서브 쿼리

예시 1

 @Test
    void subQuery(){
        QMember qmember= new QMember("memberSub");
        List<Member> result = queryFactory.select(member).from(member)
                        .where(member.age.eq(JPAExpressions.select(qmember.age.max())
                                .from(qmember))).fetch();
        assertThat(result).extracting("age").containsExactly(40);
    }
  • 나이가 가장많은 회원 조회
  • JPAExpressions 사용

예시 2

@Test
    void subQueryGoe(){
        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
    void subQueryIn(){
        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);
    }

CASE문

단순한 조건

 @Test
    void basicCase(){
        List<String> result =queryFactory.select(member.age
                        .when(10).then("10살")
                        .when(20).then("20살")
                        .otherwise("기타"))
                .from(member)
                .fetch();

        for(String str : result){
            System.out.println("s =" +str);
        }

    }

복잡한 조건

@Test
    void complexCase(){
        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();
        for(String str : result){
            System.out.println("s =" +str);
        }
    }

orderBy에서 Case문 함께 사용하기

  1. 0~30살이 아닌 사람을 가장 먼저 출력
  2. 0~20살 출력
  3. 21~30살 출력
NumberExpressions<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);

상수 더하기 -concat/constant

 @Test
    void constant(){
        List<Tuple> result = queryFactory.select(member.username, Expressions.constant("A"))
                .from(member).fetch();
        for(Tuple tuple : result){
            System.out.println("tuple : "+tuple);
        }
    }

    @Test
    void concat(){
        List<String> result = queryFactory.select(member.username.concat("_").concat(member.age.stringValue())).from(member).where(member.username.eq("member1")).fetch();

        for(String s: result){
            System.out.println("s =" +s);
        }
    }



출처: https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84

profile
자바를 사랑합니다

0개의 댓글