[JPA]/실전! Querydsl

[Querydsl] 기본 문법

쿠릉쿠릉 쾅쾅 2022. 4. 24. 13:47
728x90

 

시작 - JPQL vs Querydsl

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // JPA쿼리 팩토리를 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em); 
        
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    }

    @Test
    void startJPQL() {  // member1을 찾아라

        String qlString =
                "select m from Member m" +
                "where m.username = :username";

        Member findMember = em.createQuery(qlString, Member.class)
                .setParameter("username", "member1")
                .getSingleResult();
        assertThat(findMember.getUsername()).isEqualTo("member1");
    }

    @Test
    void startQuerydsl() {  // member1을 찾아라
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
//        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        QMember m = QMember.member;

        Member findMember = queryFactory
                .select(m)
                .from(m)
                .where(m.username.eq("member1"))  // 파라미터 바인딩을 안해도 eq() 를 통해서 자동으로 파라미터 바인딩이 된다.
                .fetchOne();

        assertThat(findMember.getUsername()).isEqualTo("member1");

    }
}

 

Querydsl은 직접 파라미터를 바인딩 하지 않아도 sql 인잭션 공격을 막기 위해 기본적으로 prepared statement의 파라미터 바인딩 방식을 사용한다.
JPAQueryFactory를 필드 레벨로 제공해도 동시성 문제가 없다. 동시성 문제는 JPAQueryFactory를 생성할 때 제공되는 EntityManager(em)에 달려있다. 그러나 스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에 동시성 문제는 걱정하지 않아도 된다. 때문에 springconfig 같은 클래스를 만들어서 JPAQueryFactory@autowired로 쓰면 엄청 편하다.

 


 

기본 Q-Type 활용

Querydsl 이란 Querydsl 을 통하여 생성되는 정적 Q-type 클래스를 이용하여 SQL 과 같은 쿼리를 생성하도록 도와주는 프레임워크이다. JPA 뿐만 아니라 MongoDB, JDO, Lucene 과 같은 라이브러리도 제공하고 있다.

앞에서 말했던 JPQL의 단점을 완벽하게 커버할 수 있는 Querydsl 은 타입에 안전한 방식으로 쿼리를 실행할 수 있다. 타입을 통하여 쿼리를 작성하므로 도메인 모델의 프로퍼티 변경에 유연하게 대처가 가능하며 강력한 코드 자동완성 기능의 이점을 얻을 수 있고 무엇보다 쿼리를 빠르고 안전하게 만들 수 있다.

Querydsl은 JPQL의 빌더 역할이다.

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

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

기본인스턴스를 사용할 때 static import를 사용할 것.
기본 인스턴스를 기본으로 사용하고 같은 테이블을 조인해서 별칭으로 서로 구별을 해줘야할 때만 new 연산자로 사용할 것.

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
    
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    }

    @Test
    void startQuerydsl() {

        Member findMember = queryFactory
                .select(member)  // QMember.member를 static import해서 사용했다.
                .from(member)
                .where(member.username.eq("member1"))  // 파라미터 바인딩을 안해도 eq() 를 통해서 자동으로 파라미터 바인딩이 된다.
                .fetchOne();

        assertThat(findMember.getUsername()).isEqualTo("member1");

    }
}

 

📌 Application.yml

spring.jpa.properties.hibernate.use_sql_comments: true

참고로 다음 설정을 추가하면 실행되는 JPQL을 볼 수 있다.

 


 

검색 조건 쿼리

1. 기본 검색 쿼리

📌 QuerydslBasicTest

@Test
void search() {
    Member findMember = queryFactory
            .selectFrom(member) // select + from
            .where(member.username.eq("member1")
                    .and(member.age.eq(10)))
            .fetchOne();

    assertThat(findMember.getUsername()).isEqualTo("member1");
}

검색 조건은 .and() / .or()를 메서드 체인으로 연결할 수 있다.
select() / from()selectFrom()으로 합칠 수 있다.

 

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

 

3.  AND 조건을 파라미터로 처리

📌 QuerydslBasicTest

@Test
void searchAndParam() {
    Member findMember = queryFactory
            .selectFrom(member) // select + from
            .where(
                    member.username.eq("member1"), 
                    member.age.eq(10) // 파라미터로 AND() 대체 가능 (이 방식을 더 선호)
            )
            .fetchOne();

    assertThat(findMember.getUsername()).isEqualTo("member1");
}

 

where()에 파라미터로 검색 조건을 추가하면 AND 조건으로 인식한다.
where()의 파라미터에 null 값이 있을 경우 null 값을 무시한다. → 이것을 통해 메서드 추출을 활용해서 동적 쿼리를 깔끔하게 만들 수 있다.
AND() 조건만 있다면 이 방식으로 할 것

 


 

결과 조회

fetch() : 리스트 조회, 데이터가 없으면 빈 리스트로 반환한다.
fetchOne() : 단 건 조회.
 - 결과가 없으면 : null
 - 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
fetchFirst() : limit(1).fetchOne();
fetch().size();
count 쿼리로 변경해서 count 수 조회

📌 QuerydslBasicTest

@Test
void resultFetch() {
    // list
    List<Member> fetch = queryFactory
            .selectFrom(member)
            .fetch();

    // 단 건
    Member member1 = queryFactory
            .selectFrom(QMember.member)
            .fetchOne();


    // 처음 한 건 조회
    Member fetchFirst = queryFactory
            .selectFrom(QMember.member)
            .fetchFirst();
}

 

🧷 참고

fetchResults() : 페이징 정보 포함. 현재 deprecated 상태.
fetchCount() : count쿼리로 변경해서 count 수 조회. 현재 deprecated 상태

위 두 기능은 현재 deprecated 된 상태다. 왜냐하면 단순한 쿼리에서는 잘 동작하지만 복잡한 쿼리에서는 잘 동작하지 않기 때문이다. 따라서 fetchResults()는 fetch()로 대체한다. 그리고 fetchCount()는 별도로 count 쿼리를 작성해야 한다. 

✔ 직접 count 쿼리 작성하기

📌 MemberCount

@Test
void count1() {
    Long totalCount = queryFactory
            .select(member.count())  // count(member.id)
            .from(member)
            .fetchOne();
}

@Test
void count2() {
    Long totalCount = queryFactory
            .select(Wildcard.count)  // select count(*)
            .from(member)
            .fetchOne();
}

count(*)을 사용하고 싶으면 Wildcard.count를 사용하면 된다.
count(member.id)를 사용하고 싶으면 member.count()를 사용하면 된다.
응답 결과는 숫자 하나이므로 fetchOne()을 사용한다.

 

 

 


 

정렬

📌 QuerydslBasicTest

/**
 * 회원 정렬 순서
 * 1. 회원 나이 내림차순 (desc)
 * 2. 회원 이름 올림차순 (asc)
 * 단 2에서 회원 이름이 없으면 마지막에 출력 (nulls last)
 */
@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();

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

}
/* select
    member1 
from
    Member member1 
where
    member1.age = ?1 
order by
    member1.age desc,
    member1.username asc nulls last */ select
        member0_.member_id as member_i1_1_,
        member0_.age as age2_1_,
        member0_.team_id as team_id4_1_,
        member0_.username as username3_1_ 
    from
        member member0_ 
    where
        member0_.age=? 
    order by
        member0_.age desc,
        member0_.username asc nulls last

Comparator 함수형 인터페이스의 static 메서드인 nullsFirst() / nullsLast() 를 이용하여 정렬시 발생하는 NullPointerException 예외를 처리할 수 있다.
nullsFirst() : null 데이터는 맨 앞에 정렬
nullsLast() : null 데이터는 맨 뒤에 정렬
orderBy() : 정렬할 항목들을 파라미터로 전달하면 된다.
desc() : 내림차순
asc() : 오름차순

 

 

 


 

페이징

📌 QuerydslBasicTest

@Test
void paging1() {
    List<Member> result = queryFactory
            .selectFrom(member)
            .orderBy(member.username.desc())
            .offset(1)  // 데이터를 가져오기 시작할 row를 지정. (페이지 번호-1) * 페이지당 데이터 건수
            .limit(2)   // 페이지당 데이터 건 수
            .fetch();

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

}

 


 

집합

1. 집합 함수

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    }

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

    }

}

Querydsl은 JPQL이 제공하는 모든 집합 함수를 제공한다.
Tuple은 querydsl이 제공하는 기능이다.
프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있지만, 프로잭션 대상이 둘 이상이면 Tuple이나 DTO로 조회해야 한다. 이처럼 Tuple은 여러 개의 타입을 받을 때 사용한다.
실무에서 Tuple이 자주 쓰이지는 않는다. DTO로 뽑아오는 방식을 더 많이 선호한다.

 

2. GroupBy 사용

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    }

    /**
     * 팀의 이름과 각 팀의 평균 연령을 구해라.
     */
    @Test
    void group() {
        List<Tuple> result = queryFactory
                .select(
                        team.name,
                        member.age.avg()
                )
                .from(member)
                .join(member.team, team)  // member에 있는 team과 team을 조인한다.
                .groupBy(team.name)  // team의 이름으로 그룹핑을 한다.
                .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);  // (10+20) / 2

        assertThat(teamB.get(team.name)).isEqualTo("teamB");
        assertThat(teamB.get(member.age.avg())).isEqualTo(35);  // (30+40) / 2

    }
}

groupBy() 메서드를 통해 그룹핑을 할 수 있다.
그룹화된 결과를 제한하려면 having()을 사용하면 된다.

🔍 groupBy(), having() 예시

 …
    .groupBy(item.price)  // 아이템 가격으로 그룹핑
    .having(item.price.gt(1000))  // 아이템 가격이 1000 초과일 때
...

 


 

조인 

1. 기본 조인

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

조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q타입을 지정하면 된다.

🔍 조인 문법

join() / innerJoin() : 내부 조인 (inner join)
leftJoin() : left 외부 조인 (left outer join)
rightJoin() : right 외부 조인 (right outer join)

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    /**
     * 팀 A에 소속된 모든 회원
     */
    @Test
    void join() {
        List<Member> result = queryFactory
                .selectFrom(member)
                .join(member.team, team)
                .where(team.name.eq("teamA"))
                .fetch();

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

}

extracting(String propertyOrField) : 검증하고자 하는 값이 특정 객체에 속해있을 때 해당 프로퍼티의 이름(변수명)을 파라티머로 넣으면 된다.  참고로 파라미터에 람다도 받을 수 있고, 여러 가지로 오버로딩이 되어있다.
containsExactly(Object... value) : 파라미터로 주어진 값들을 순서대로 포함하고 있는지 확인한다.

extracting 메서드 참고 : https://velog.io/@byeongju/AssertJ%EC%9D%98-extracting

AssertJ 공식 문서 : https://assertj.github.io/doc/

🔍 세타 조인

세타 조인은 연관관계가 없는 필드로 조인하는 것이다.

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    /**
     * 세타 조인
     * 회원의 이름이 팀 이름과 같은  회원 조회
     */
    @Test
    void theta_join() {
        em.persist(new Member("teamA"));  // 사람 이름이 teamA
        em.persist(new Member("teamB"));  // 사람 이름이 teamB
        em.persist(new Member("teamC"));  // 사람 이름이 teamC

        List<Member> result = queryFactory
                .select(member)
                .from(member, team)  // 모든 멤버와 팀을 다 가져와서 조인하는 것 (디비가 자체적으로 성능 최적화한다.)
                .where(member.username.eq(team.name))
                .orderBy(member.username.asc())  // username을 오름차순으로 정렬
                .fetch();

        assertThat(result)
                .extracting("username")
                .containsExactly("teamA", "teamB");
    }
}

from절에 여러 엔티티를 선택해서 세타 조인을 했다.
세타 조인은 외부 조인이 불가능하다. 그러나 조인 on을 사용하면 외부 조인을 할 수 있다.

 

2. 조인 - on절

🔍 조인 - on 절 기능

  • 조인 대상 필터링을 한다.
  • 연관관계 없는 엔티티를 외부 조인한다. (자주 쓰임)

🔍 조인 대상 필터링

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    /**
     * 예) 회원과 팀을 조인하면서, 팀 이름이 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
    void join_on_filtering() {
        List<Tuple> result = queryFactory
                .select(member, team)  // select 파라미터가 여러개이므로 결과 타입이 Tuple이다.
                .from(member)
                .leftJoin(member.team, team).on(team.name.eq("teamA"))
                .fetch();

        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
        }
    }

}
tuple = [Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
tuple = [Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
tuple = [Member(id=5, username=member3, age=30), null]
tuple = [Member(id=6, username=member4, age=40), null]

left join이므로 일단 Member 데이터를 다 가져온다. 그리고 Team 데이터는 Team 이름이 "teamA"인 경우만 데이터를 가져온다.
on절을 활용해 조인 대상을 필터링할 때, 외부조인이 아니라 내부 조인을 사용하면 where 절에서 필터링하는 것과 기능이 동일하다. 따라서 on절을 활용한 조인 대상 필터링을 사용할 때, 내부조인이면 익숙한 where절로 해결하고 정말 외부조인이 필요한 경우에만 on절을 사용할 것.

💡 innerJoin의 where 와 on이 서로 같은지 알아보기

✔ innerJoin-where

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

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

        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
        }
    }
}
    /* select
        member1,
        team 
    from
        Member member1   
    inner join
        member1.team as team 
    where
        team.name = ?1 */ select
            member0_.member_id as member_i1_1_0_,
            team1_.team_id as team_id1_2_1_,
            member0_.age as age2_1_0_,
            member0_.team_id as team_id4_1_0_,
            member0_.username as username3_1_0_,
            team1_.name as name2_2_1_ 
        from
            member member0_ 
        inner join
            team team1_ 
                on member0_.team_id=team1_.team_id 
        where
            team1_.name=?
            
tuple = [Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
tuple = [Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]

✔ innerJoin-on

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);
    }
    
    @Test
    void innerJoin_on() {
        List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .join(member.team, team).on(team.name.eq("teamA"))
                .fetch();

        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
        }
    }
}
    /* select
        member1,
        team 
    from
        Member member1   
    inner join
        member1.team as team with team.name = ?1 */ select
            member0_.member_id as member_i1_1_0_,
            team1_.team_id as team_id1_2_1_,
            member0_.age as age2_1_0_,
            member0_.team_id as team_id4_1_0_,
            member0_.username as username3_1_0_,
            team1_.name as name2_2_1_ 
        from
            member member0_ 
        inner join
            team team1_ 
                on member0_.team_id=team1_.team_id 
                and (
                    team1_.name=?
                )
                
tuple = [Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
tuple = [Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]

✔ 결론
내부조인을 사용했을 때 on절과 where절의 출력 결과가 서로 같다는 것을 알 수 있다.

🔍 연관관계가 없는 엔티티 외부 조인

하이버네이트 5.1 버전부터 on()을 이용해서 서로 관계가 없는 필드로 외부 조인하는 기능이 추가됐다.
물론 내부 조인도 가능하다. 

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    /**
     * 연관관계가 없는 엔티티를 외부 조인
     * 회원의 이름이 팀 이름과 같은 대상 외부 조인
     */
    @Test
    void join_on_no_relation() {
        em.persist(new Member("teamA"));  // 사람 이름이 teamA
        em.persist(new Member("teamB"));  // 사람 이름이 teamB
        em.persist(new Member("teamC"));  // 사람 이름이 teamC

        List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .leftJoin(team).on(member.username.eq(team.name))  // 연관관계가 없는 조인이므로 on절 조건 맞는 것만 조인이 된다.
                .fetch();

        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
        }
    }
}
    /* select
        member1,
        team 
    from
        Member member1   
    left join
        Team team with member1.username = team.name */ select
            member0_.member_id as member_i1_1_0_,
            team1_.team_id as team_id1_2_1_,
            member0_.age as age2_1_0_,
            member0_.team_id as team_id4_1_0_,
            member0_.username as username3_1_0_,
            team1_.name as name2_2_1_ 
        from
            member member0_ 
        left outer join
            team team1_ 
                on (
                    member0_.username=team1_.name
                )
                
tuple = [Member(id=3, username=member1, age=10), null]
tuple = [Member(id=4, username=member2, age=20), null]
tuple = [Member(id=5, username=member3, age=30), null]
tuple = [Member(id=6, username=member4, age=40), null]
tuple = [Member(id=7, username=teamA, age=0), Team(id=1, name=teamA)]
tuple = [Member(id=8, username=teamB, age=0), Team(id=2, name=teamB)]
tuple = [Member(id=9, username=teamC, age=0), null]

연관관계가 없는 엔티티를 조인할 때는 조심해야할 점이 있다.
leftJoin() 메서드의 파라미터를 보면 일반 조인과 다르게 엔티티가 하나만 존재한다.
- 일반 조인 : leftJoin(member.team, team)
- on 조인 : from(member).leftJoin(team).on(....) 
on 조인 처럼 연관관계가 없을시에 leftJoin() 파라미터는 무조건 엔티티가 하나다.

 

3. 조인 - 페치조인

페치조인은 SQL에서 제공하는 기능은 아니다.
SQL 조인을 활용해서 연관된 엔티티를 SQL 한번에 조회하는 기능이다.
주로 성능 최적화에 사용하는 방법이다.

🔍 페치 조인 미적용

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    @PersistenceUnit
    EntityManagerFactory emf;

    @Test
    void fetchJoinNo() {
        // 영속성 컨텍스트 초기화
        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();
        System.out.println("findMember = " + findMember.getTeam().getName());
    }
}
    /* select
        member1 
    from
        Member member1   
    inner join
        member1.team as team 
    where
        member1.username = ?1 
        and member1.team.name = ?2 */ select
            member0_.member_id as member_i1_1_,
            member0_.age as age2_1_,
            member0_.team_id as team_id4_1_,
            member0_.username as username3_1_ 
        from
            member member0_ 
        inner join
            team team1_ 
                on member0_.team_id=team1_.team_id 
        where
            member0_.username=? 
            and team1_.name=?

// ----------------------------------------

    select
        team0_.team_id as team_id1_2_0_,
        team0_.name as name2_2_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
        
findMember = teamA

지연로딩으로 Member, Team SQL 쿼리를 각각 실행된다. (N+1 문제 발생)

🔍 페치 조인 적용

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);
    }
    
    @PersistenceUnit
    EntityManagerFactory emf;

    @Test
    void fetchJoinUse() {
        // 영속성 컨텍스트 초기화
        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();
        System.out.println("findMember = " + findMember.getTeam().getName());

    }
}
    /* select
        member1 
    from
        Member member1   
    inner join
        fetch member1.team as team 
    where
        member1.username = ?1 */ select
            member0_.member_id as member_i1_1_0_,
            team1_.team_id as team_id1_2_1_,
            member0_.age as age2_1_0_,
            member0_.team_id as team_id4_1_0_,
            member0_.username as username3_1_0_,
            team1_.name as name2_2_1_ 
        from
            member member0_ 
        inner join
            team team1_ 
                on member0_.team_id=team1_.team_id 
        where
            member0_.username=?
            
findMember = teamA

fetchJoin()을 통해 즉시로딩으로 바꿔서 Member, Team를 한 번의 쿼리로 다 가져온다. (N+1 문제 해결)

 


 

서브 쿼리

com.querydsl.jpa.JPAExpressions 를 사용하여 서브쿼리를 생성한다. (static import 할 것을 권장)
db는 데이터만 필터링하고 그룹핑하고 조회만 하고 로직은 다 애플리케이션에서 해결해야한다.
ORM을 사용하며, 객체지향적으로 엔티티가 구성되어 있으면 서브쿼리가 필요한 일은 거의 없다.
애초에 서브쿼리는 성능이 안좋기 때문에 안티패턴이다.

1. 서브 쿼리 eq 사용 (where절 서브 쿼리)

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    /**
     * 나이가 가장 많은 회원 조회
     */
    @Test
    void subQuery() {

        QMember memberSub = new QMember("memberSub");  // 별칭(alias)가 겹치면 안되기 때문에 새로운 인스턴스 생성.

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

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

}
/* select
    member1 
from
    Member member1 
where
    member1.age = (
        select
            max(memberSub.age) 
        from
            Member memberSub
    ) */ select
        member0_.member_id as member_i1_1_,
        member0_.age as age2_1_,
        member0_.team_id as team_id4_1_,
        member0_.username as username3_1_ 
    from
        member member0_ 
    where
        member0_.age=(
            select
                max(member1_.age) 
            from
                member member1_
        )

위의 서브 쿼리를 쿼리 2개로 나눠봤다.

📌 QuerydslBasicTest

/**
 * 서브 쿼리를 2개의 쿼리로 분리
 */
@Test
void subQueryChangeTwoQuery() {
    Integer maxAge = queryFactory  // 최대 나이 구하기
            .select(member.age.max())
            .from(member)
            .fetchOne();

    System.out.println("maxAge = " + maxAge);

    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.eq(maxAge))
            .fetch();

    System.out.println("result = " + result);
}
/* select
    max(member1.age) 
from
    Member member1 */ select
        max(member0_.age) as col_0_0_ 
    from
        member member0_
        
maxAge = 40

// --------------------------------------

/* select
    member1 
from
    Member member1 
where
    member1.age = ?1 */ select
        member0_.member_id as member_i1_1_,
        member0_.age as age2_1_,
        member0_.team_id as team_id4_1_,
        member0_.username as username3_1_ 
    from
        member member0_ 
    where
        member0_.age=?
        
result = [Member(id=6, username=member4, age=40)]

 

2. 서브 쿼리 goe 사용 (where절 서브 쿼리)

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    /**
      * 나이가 평균 이상인 회원 조회
      */
     @Test
     void subQueryGoe() {

         QMember memberSub = new QMember("memberSub");  // 별칭(alias)가 겹치면 안되기 때문에 새로운 인스턴스 생성.

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

         assertThat(result).extracting("age")
                 .containsExactly(30, 40);
     }
}
/* select
    member1 
from
    Member member1 
where
    member1.age >= (
        select
            avg(memberSub.age) 
        from
            Member memberSub
    ) 
order by
    member1.age asc */ select
        member0_.member_id as member_i1_1_,
        member0_.age as age2_1_,
        member0_.team_id as team_id4_1_,
        member0_.username as username3_1_ 
    from
        member member0_ 
    where
        member0_.age>=(
            select
                avg(cast(member1_.age as double)) 
            from
                member member1_
        ) 
    order by
        member0_.age asc

서브쿼리를 2개의 쿼리로 분리시켰다.

📌 QuerydslBasicTest

/**
 * 서브 쿼리를 2개로 분리
 */
@Test
void subQueryGoeChangeTwoQuery() {
    List<Double> avgAge = queryFactory
            .select(member.age.avg())
            .from(member)
            .fetch();

    System.out.println("avgAge = " + avgAge);

    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.goe(avgAge))
            .fetch();

    System.out.println("result = " + result);
}
/* select
    avg(member1.age) 
from
    Member member1 */ select
        avg(cast(member0_.age as double)) as col_0_0_ 
    from
        member member0_
            
avgAge = 25.0

// ----------------------------------------------

/* select
    member1 
from
    Member member1 
where
    member1.age >= ?1 */ select
        member0_.member_id as member_i1_1_,
        member0_.age as age2_1_,
        member0_.team_id as team_id4_1_,
        member0_.username as username3_1_ 
    from
        member member0_ 
    where
        member0_.age>=?
        
result = [Member(id=5, username=member3, age=30), Member(id=6, username=member4, age=40)]

 

3. 서브쿼리 여러 건 처리 in 사용 (where절 서브쿼리)

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    @Test
     void subQueryIn() {

         QMember memberSub = new QMember("memberSub");  // 별칭(alias)가 겹치면 안되기 때문에 새로운 인스턴스 생성.

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

         assertThat(result).extracting("age")
                 .containsExactly(20, 30, 40);
     }
}
/* select
    member1 
from
    Member member1 
where
    member1.age in (
        select
            memberSub.age 
        from
            Member memberSub 
        where
            memberSub.age > ?1
    ) 
order by
    member1.age asc */ select
        member0_.member_id as member_i1_1_,
        member0_.age as age2_1_,
        member0_.team_id as team_id4_1_,
        member0_.username as username3_1_ 
    from
        member member0_ 
    where
        member0_.age in (
            select
                member1_.age 
            from
                member member1_ 
            where
                member1_.age>?
        ) 
    order by
        member0_.age asc

📌 QuerydslBasicTest

 

@Test
void subQueryInChangeTwoQuery() {
    List<Integer> gtAge10 = queryFactory
            .select(member.age)
            .from(member)
            .where(member.age.gt(10))
            .fetch();

    System.out.println("gtAge10 = " + gtAge10);

    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.in(gtAge10))
            .fetch();

    System.out.println("result = " + result);

}
/* select
    member1.age 
from
    Member member1 
where
    member1.age > ?1 */ select
        member0_.age as col_0_0_ 
    from
        member member0_ 
    where
        member0_.age>?

gtAge10 = [20, 30, 40]

// ------------------------------------------------

/* select
    member1 
from
    Member member1 
where
    member1.age in ?1 */ select
        member0_.member_id as member_i1_1_,
        member0_.age as age2_1_,
        member0_.team_id as team_id4_1_,
        member0_.username as username3_1_ 
    from
        member member0_ 
    where
        member0_.age in (
            ? , ? , ?
        )

result = [Member(id=4, username=member2, age=20), Member(id=5, username=member3, age=30), Member(id=6, username=member4, age=40)]

 

4. select절에 서브쿼리

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    /**
     * 유저 이름과 유저들의 평균 나이를 조회
     */
    @Test
     void selectSubQuery() {

         QMember memberSub = new QMember("memberSub");  // 별칭(alias)가 겹치면 안되기 때문에 새로운 인스턴스 생성.

         List<Tuple> result = queryFactory
                 .select(member.username,
                         JPAExpressions
                                 .select(memberSub.age.avg())
                                 .from(memberSub)
                 )
                 .from(member)
                 .fetch();

         for (Tuple tuple : result) {
             System.out.println("tuple = " + tuple);
         }
     }
}
/* select
    member1.username,
    (select
        avg(memberSub.age) 
    from
        Member memberSub) 
from
    Member member1 */ select
        member0_.username as col_0_0_,
        (select
            avg(cast(member1_.age as double)) 
        from
            member member1_) as col_1_0_ 
    from
        member member0_

tuple = [member1, 25.0]
tuple = [member2, 25.0]
tuple = [member3, 25.0]
tuple = [member4, 25.0]

 

 

5. from절의 서브쿼리 한계

JPA JPQL 서브쿼리의 한계점으로 from절의 서브쿼리(인라인 뷰)는 지원하지 않는다. 당연히 Querydsl도 지원하지 않는다. 

🔍 from절의 서브 쿼리 해결방안

  • 서브쿼리를 join으로 변경한다. (가능할 때도 있고, 불가능할 때도 있다.)
  • 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
  • nativeSQL을 사용한다.

참고로 db는 데이터만 필터링하고 그룹핑하고 조회만 하고 로직은 다 애플리케이션에서 해결해야한다.

 


 

Case 문

case문은 select / 조건절(where) / order by 에서 사용 가능하다.
그러나 case의 계산 조건들은 DB에서 하는 것 보다 애플리케이션에서 로직을 작성하는 것이 좋다. 

1. 단순한 조건

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    @Test
    void basicCase() {
        List<String> result = queryFactory
                .select(member.age
                        .when(10).then("열살")  // 10살일 때
                        .when(20).then("스무살")  // 20살일 때
                        .otherwise("기타")  // 나머지
                )
                .from(member)
                .fetch();

        System.out.println("result = " + result);
    }
}
    /* select
        case 
            when member1.age = ?1 then ?2 
            when member1.age = ?3 then ?4 
            else '기타' 
        end 
    from
        Member member1 */ select
            case 
                when member0_.age=? then ? 
                when member0_.age=? then ? 
                else '기타' 
            end as col_0_0_ 
        from
            member member0_
            
result = [열살, 스무살, 기타, 기타]

 

2. 복잡한 조건

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    @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();

        System.out.println("result = " + result);
    }
}
    /* select
        case 
            when (member1.age between ?1 and ?2) then ?3 
            when (member1.age between ?4 and ?5) then ?6 
            else '기타' 
        end 
    from
        Member member1 */ select
            case 
                when member0_.age between ? and ? then ? 
                when member0_.age between ? and ? then ? 
                else '기타' 
            end as col_0_0_ 
        from
            member member0_
            
result = [0~20살, 0~20살, 21~30살, 기타]

 

3. 예제

예를들어서 다음과 같이 임의의 순서로 회원을 출력하고 싶다면?
- 1. 0 ~ 30살이 아닌 회원을 가장 먼저 출력
- 2. 0 ~ 20살 회원 출력
- 3. 21 ~ 30 살 회원 출력 

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    @Test
    void anotherComplexCase() {
        NumberExpression<Integer> rankPath = new CaseBuilder()
                .when(member.age.between(0, 20)).then(2)
                .when(member.age.between(21, 30)).then(1)
                .otherwise(3);

        System.out.println("rankPath = " + rankPath);

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

}
rankPath = case when member1.age between 0 and 20 then 2 when member1.age between 21 and 30 then 1 else 3 end

    /* select
        member1.username,
        member1.age,
        case 
            when (member1.age between ?1 and ?2) then ?3 
            when (member1.age between ?4 and ?5) then ?6 
            else 3 
        end 
    from
        Member member1 
    order by
        case 
            when (member1.age between ?7 and ?8) then ?9 
            when (member1.age between ?10 and ?11) then ?12 
            else 3 
        end desc */ select
            member0_.username as col_0_0_,
            member0_.age as col_1_0_,
            case 
                when member0_.age between ? and ? then ? 
                when member0_.age between ? and ? then ? 
                else 3 
            end as col_2_0_ 
        from
            member member0_ 
        order by
            case 
                when member0_.age between ? and ? then ? 
                when member0_.age between ? and ? then ? 
                else 3 
            end desc
            
username = member4 age = 40 rank = 3
username = member1 age = 10 rank = 2
username = member2 age = 20 rank = 2
username = member3 age = 30 rank = 1

 


 

상수, 문자 더하기

1. 상수 더하기

상수가 필요하면 Expressions.constant(xxx) 메서드를 사용하면 된다.

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    @Test
    void constant() {
        List<Tuple> result = queryFactory
                .select(member.username, Expressions.constant("A"))
                .from(member)
                .fetch();

        System.out.println("result = " + result);

    }
}
    /* select
        member1.username 
    from
        Member member1 */ select
            member0_.username as col_0_0_ 
        from
            member member0_
            
result = [[member1, A], [member2, A], [member3, A], [member4, A]]

 

상수는 JPQL 쿼리에서 안나가고 결과에서만 추가해서 나간다.

 

2. 문자 더하기 concat

📌 QuerydslBasicTest

package study.querydsl;

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    // 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
    JPAQueryFactory queryFactory;

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before() {
        // JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
        queryFactory = new JPAQueryFactory(em);

        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);

        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

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

    @Test
    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);
        }
    }
}
    /* select
        concat(concat(member1.username,
        ?1),
        str(member1.age)) 
    from
        Member member1 
    where
        member1.username = ?2 */ select
            ((member0_.username||?)||cast(member0_.age as char)) as col_0_0_ 
        from
            member member0_ 
        where
            member0_.username=?
            
s = member1_10

member.age.stringValue() 부분이 중요하다. 문제가 아닌 다른 타입들은 stringValue()를 통해 문자로 변환할 수 있다. 이 방법은 ENUM을 문자열로 바꿀 때 자주 사용한다.

 

 

 

 

 


👀 참고 자료

https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84/dashboard

 

실전! Querydsl - 인프런 | 강의

Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!, - 강의 소개 | 인프런...

www.inflearn.com

 

https://devwithpug.github.io/java/querydsl-with-datajpa/

 

Querydsl으로 안전한 쿼리 작성하기 + DataJPA

개요

devwithpug.github.io

 

https://jojoldu.tistory.com/379

 

[Querydsl] 서브쿼리 사용하기

안녕하세요! 이번 시간에는 Querydsl에서의 Subquery 기본 가이드를 진행합니다. 개인적으로 ORM을 사용하며, 객체지향적으로 엔티티가 구성되어있으면 서브쿼리가 필요한 일은 거의 없다고 생각하

jojoldu.tistory.com

 

 

 

728x90