Spring Boot/QueryDSL

Spring Boot QueryDSL 기본 문법

최-코드 2024. 8. 13. 10:26
...
//웬만하면 이 방식으로 QClass 만들자.
import static com.rosoa0475.querydsl.entity.QMember.member;

...

@Autowired
EntityManager em;
JPAQueryFactory queryFactory;

@BeforEach
public void befor(){
	queryFactory = new JPAQueryFactory(em);
    ...
}

public void startQuerydsl(){
//  QMember m1 = new QMember("m1") 같은 테이블이 서로 조인하려고 할 때는 이 방식으로 다른 값을 넣어야 한다. 테이블에 대한 별칭이 되기 때문이다.
    Member findMember = queryFactory
            .select(member)
            .from(member)
            .where(member.username.eq("member1")) // 파라미터 바인딩 처리까지 해준다.(?1와 같은 것에 대해)
            .fetchOne();
    assertThat(findMember.getUsername()).isEqualTo("member1");
}

JPAQueryFactory에 EntityManager를 넣어서 JPA와 같이 queryFactory가 내부적으로 em을 통해 CRUD를 진행한다. 

 

검색 조건 함수

 

사용 예제

@Test
public void search(){
    Member findMember = queryFactory
            .selectFrom(member)
            .where(member.username.eq("member1"),
                    /*.and(member.age.eq(10))*/
                    member.age.eq(10))
            .fetchOne();
    assertThat(findMember.getUsername()).isEqualTo("member1");
}
  • and의 경우 .and 대신에 ,을 통해서도 이루어질 수 있다.
  • ,로 and 처리할 시에 만약 null 값이 있을 경우 해당 null 부분은 무시해버린다. 즉, 쿼리에 아예 포함이 안 된다.

 

결과 조회

@Test
public void resultFetch(){
    //리스트로 조회, 데이터 없으면 빈 리스트 반환한다.
    List<Member> fetch = queryFactory
            .selectFrom(member)
            .fetch();
    //단 건 조회, 결과가 없으면 null이고, 둘 이상이면 NonUniqueResultException 발생한다.
    Member fetchOne = queryFactory
            .selectFrom(member)
            .fetchOne();

    Member fetchFirst = queryFactory
            .selectFrom(member)
            .fetchFirst();// limit(1).fetchOne()과 같다.
}

 

정렬

/*
1. 회원 나이 내림차순
2. 회원 이름 올림차순
단 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(member6.getUsername()).isEqualTo("member6");
    assertThat(memberNull.getUsername()).isNull();
}

.nullsLast()와 .nullsFirst()를 통해 null 데이터의 순서를 부여할 수 있다. .nullsLast()를 지정할 시에 null값은 정렬에서 맨 뒷 순서다.

 

페이징

@Test
public void paging(){
    List<Member> result = queryFactory
            .selectFrom(member)
            .orderBy(member.username.desc())
            .offset(1) // 0번, 1번 데이터 스킵
            .limit(2) // 2개 데이터 선택 시 바로 반환
            .fetch();

    assertThat(result.size()).isEqualTo(2);
}

 

집합

@Test
public void aggregation(){
    //QueryDSL이 제공하는 Tuple 타입 - 여러 개의 타입을 저장할 수 있다.
    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);
}

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

 

having 절을 통해 그룹화된 결과를 제한할 수 있다.

 

조인

  • .join()에서 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에는 별칭으로 사용할 Q타입을 지정하면 된다.
  • .join(member.team, team)와 같이 하면 jpql은 member.team as team와 같이 설정 된다.
/**
 * 팀 A에 소속된 모든 회원
 */
@Test
public void join() throws Exception {
    List<Member> result = queryFactory
            .selectFrom(member)
            .join(member.team, team)
            //.leftJoin(member.team, team) = .join()
            //.rightJoin(member.team, team)
            //.innerJoin(member.team, team)
            .where(team.name.eq("teamA"))
            .fetch();

    assertThat(result)
            .extracting("username")
            .containsExactly("member1", "member2");
}

/**
 * 세타 조인, 카테시안 곱
 * 회원의 이름이 팀 이름과 같은 회원 조회
 */
@Test
public void theta_join() throws Exception {
    em.persist(new Member("teamA"));
    em.persist(new Member("teamB"));
    em.persist(new Member("teamC"));

    List<Member> result = queryFactory
            .select(member)
            .from(member, team)
            .where(member.username.eq(team.name))
            .fetch();

    assertThat(result)
            .extracting("username")
            .containsExactly("teamA", "teamB");
}
  • on절 활용
    • 조인 대상 필터링
      • 외부 조인에 대해서만 on절에 썻을 때와 where절에서 썻을 때의 차이점이 존재한다.
      • 해당 조건에서 만족하는 거만 조인하므로 조건을 만족하지 않은 것에 대해 null로 배치된다.
      • 아래의 경우 teamB는 null값으로 나온다.
/**
 * 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
 * JPQL : select m, t from Member m left join m.team t on 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);
    }
}

  • on절 활용
    • 연관관계가 없는 엔티티 외부조인
      • 일반적으로 jpa에서 join할 때 obj1.obj2와 같이 한다. 이를 통해 sql에서 on절에 pk와 fk의 값을 비교하는 구문이 들어간다.
      • 하지만 그냥 obj2만 사용하면 pk와 fk를 비교하지 않고 지정한 on절에 대해서만 조인을 진행한다.
/**
 * 연관관계가 없는 엔티티 외부조인
 * 회원의 이름이 팀 이름과 같은 대상 외부 조인
 */
@Test
public void join_on_no_relation() throws Exception {
    em.persist(new Member("teamA"));
    em.persist(new Member("teamB"));
    em.persist(new Member("teamC"));

    List<Tuple> result = queryFactory
            .select(member, team)
            .from(member)
            .leftJoin(team).on(member.username.eq(team.name))
            .where(member.username.eq(team.name))
            .fetch();

    for (Tuple tuple : result) {
        System.out.println("tuple = " + tuple);
    }
  • 페치조인
@Test
public void fetchJoinUse() throws Exception {
    em.flush();
    em.clear();

    Member findMember = queryFactory
            .selectFrom(member)
            //fetch join 시에는 별칭 X
            .join(member.team).fetchJoin()
            .where(member.username.eq("member1"))
            //oneToMnay 조인시 jpql과 같이 distinct() 추가하기
            .fetchOne();
}

 

서브쿼리

  • 서브쿼리에 com.querydsl.jpa.JPAExpressions을 사용하면 된다. QClass와 마찬가자로 스태틱 임포트 가능하다.
  • 같은 테이블에 대해서는 다른 별칭을 줘야 하므로 QClass를 다른 이름으로 만들어야 한다.
  • select 절과 where절에서만 서브쿼리가 가능하다. from절에서는 서브쿼리가 안 된다.
    • from절의 서브쿼리 해결방안, 위에서부터 1순위이다.
      • 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
      • 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
      • nativeSQL을 사용한다
...

//서브쿼리 스태틱 임포트
import static com.querydsl.jpa.JPAExpressions.select;

...

/**
 * 나이가 가장 많은 회원 조회
 */
@Test
public void subQuery() throws Exception {
    QMember memberSub = new QMember("memberSub");

    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.eq(
                    select(memberSub.age.max())
                            .from(memberSub)
            ))
            .fetch();

    assertThat(result).extracting("age")
            .containsExactly(40);
}

/**
 * 나이가 평균 이상인 회원 조회
 */
@Test
public void subQueryGoe() throws Exception {
    QMember memberSub = new QMember("memberSub");

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

    assertThat(result).extracting("age")
            .containsExactly(40);
}

/**
 *
 */
@Test
public void subQueryIn() throws Exception {
    QMember memberSub = new QMember("memberSub");

    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.in(
                    select(memberSub.age)
                            .from(memberSub)
                            .where(memberSub.age.gt(10))
            ))
            .fetch();

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

@Test
public void selectSubQuery() throws Exception {
    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);
    }

}

 

case문

  • 웬만하면 디비에 있는 로우데이터를 필터링하고 그룹화하는, 데이터를 줄이는 일만 하고 데이터를 변경하는 일은 지양해야한다. 따라서 애플리케이션에서 로직을 처리하는 것을 지향하자. 예외적으로 효율이 좋아지는 경우도 있긴 하다.
  • select, where, orber by에서 사용 가능하다.
@Test
public void basicCase() throws Exception {
    List<String> result = queryFactory
            .select(member.age
                    .when(10).then("열살")
                    .when(20).then("스무살")
                    .otherwise("기타"))
            .from(member)
            .fetch();

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

@Test
public 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 s : result) {
        System.out.println("s = " + s);
    }
}
  • querydsl은 자바코드로 작성하기 때문에 복잡한 조건을 변수로 선언해서 각 절에서 재사용할 수 있다.
@Test
public void orberByCase(){
    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); }
}

 

상수 & 문자 더하기

  • 상수의 경우 com.querydsl.core.types.dsl.Expressions의 constant() 메소드를 이용하면 된다.
@Test
public void constant(){
    List<Tuple> result = queryFactory
            .select(member.username, Expressions.constant("A"))
            .from(member)
            .fetch();

    for (Tuple tuple : result) {
        System.out.println("tuple = "+tuple);
    }
}
  • 문자 더하기의 경우 concat() 메소드를 이용하면 된다.
  • 이 때 concat()은 string값만 인자로 받기 때문에 string값이 아닌 컬럼의 경우 stringValue()메소드를 이용해야 한다. 이 때 sql에서는 cast 함수가 실행된다.
@Test
public void concat() {
    //{username}_{age}
    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);
    }
}