BACK/JPA

[JPA] Querydsl 기본 문법 (검색, 조회, 정렬, 페이징, 집합, 조인등)

연듀 2024. 3. 15. 21:07

기본 문법

 

JPQL vs Querydsl

@SpringBootTest
@Transactional
public class QuerydslBasicTest {
    @Autowired
    EntityManager em;

    JPAQueryFactory queryFactory;

    @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
    public void startJPQL(){
        // member1 을 찾아라
        Member findMember = em.createQuery("select m from Member m " +
                        "where m.username = :username", Member.class)
                .setParameter("username", "member1")
                .getSingleResult();

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

    @Test
    public void startQuerydsl(){
        //JPAQueryFactory queryFactory = new JPAQueryFactory(em); // JPAQueryFactory를 만들 때 entity manager를 생성자로 넘겨줘야 함
        QMember m = new QMember("m"); // 변수 명으로 별칭을 준다.
        Member findMember = queryFactory
                .select(m)
                .from(m)
                .where(m.username.eq("member1")) // prepared statement에 파라미터 바인딩을 자동으로 해준다.
                .fetchOne();
        // 오타가 있을 경우 컴파일 시점에 오류가 잡힌다.

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

}

 

 EntityManagerJPAQueryFactory를 생성한다.

Querydsl은 JPQL 빌더이다. 

 

기본 Q-Type 활용

    @Test
    public void startQuerydsl(){
        QMember m = QMember.member; // static 변수 활용
        Member findMember = queryFactory
                .select(m)
                .from(m)
                .where(m.username.eq("member1")) 
                .fetchOne();
    

        assertThat(findMember.getUsername()).isEqualTo("member1");
    }
       Member findMember = queryFactory
                .select(member)
                .from(member)
                .where(member.username.eq("member1")) // prepared statement에 파라미터 바인딩을 자동으로 해준다.
                .fetchOne();

 

static import 를 사용해 간단하게 QMember.member를 member로 쓸 수도 있다. (권장)

QMember m1 = new QMember("m1"); 

 

이렇게 별칭을 썼을 경우 jpql 에 별칭이 m1으로 나간다.

같은 테이블을 조인하는 경우 별칭이 같으면 안되므로 이렇게 별칭을 주어 사용할 수 있다.

 

검색 조건 쿼리

 

    @Test
    public void search(){ // 기본 검색 쿼리 
        Member findMember = queryFactory
                .selectFrom(member)
                .where(member.username.eq("member1")
                        .and(member.age.between(10, 30)))
                .fetchOne();

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

    @Test
    public void searchAndParam(){  // and 조건을 파라미터로 처리 
        Member findMember = queryFactory
                .selectFrom(member)
                .where( // and 로 조인됨
                        member.username.eq("member1"),
                        member.age.eq(10)
                )
                .fetchOne();

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

 

 

검색 조건 

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

 

결과 조회

    @Test
    public void resultFetch(){
        // List 조회, 데이터 없으면 빈 리스트 반환
        List<Member> fetch=queryFactory
                .selectFrom(member)
                .fetch();

        // 단건
        Member fetchOne = queryFactory
                .selectFrom(member)
                .fetchOne(); // 결과가 둘 이상이면 exception

        // 처음 한 건 조회
        Member fetchFirst = queryFactory
                .selectFrom(member)
                .fetchFirst(); // limit(1).fetchOne()

        // 페이징에서 사용
        QueryResults<Member> results = queryFactory
                .selectFrom(member)
                .fetchResults(); // 페이징 정보 포함, total count 쿼리 추가 실행

        results.getTotal(); // count 쿼리
        List<Member> content = results.getResults(); // 컨텐츠 가져오는 쿼리

        // count 쿼리로 변경
        long count = queryFactory
                .selectFrom(member)
                .fetchCount(); // count 용 쿼리로 바꿔 select 절을 count 쿼리로 바꿈

    }

 

정렬

    /**
     * 회원 정렬 순서
     * 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();
    }

 

페이징

    @Test
    public void paging1(){ // 조회 건수 제한
        List<Member> result = queryFactory
                .selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1)
                .limit(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 쿼리가 실행되니 성능상 주의 

 

참고: 실무에서 페이징 쿼리를 작성할 때, 데이터를 조회하는 쿼리는 여러 테이블을 조인해야 하지만, count 쿼리 는 조인이 필요 없는 경우도 있다.

그런데 이렇게 자동화된 count 쿼리는 원본 쿼리와 같이 모두 조인을 해버리기 때문에 성능이 안나올 수 있다.

count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면, count 전용 쿼리를 별도로 작성하면 된다. 

 

집합

    /**
     * 팀의 이름과 각 팀의 평균 연령 구하기
     */
    @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);

    }

 

조인 - 기본 조인

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

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

 

기본은 inner Join 이다.

 

 

세타 조인

연관관계가 없는 필드로 조인

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

        List<Member> result = queryFactory // 모든 팀과 멤버 테이블을 조인해 가져와 where 절에서 필터링 (DB가 성능 최적화)
                .select(member)
                .from(member, team)
                .where(member.username.eq(team.name))
                .fetch();

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

 

from 절에 여러 엔티티를 선택해서 세타 조인할 수 있다.

예전엔 외부 조인이 불가능 했는데, 이젠 on 절으로 외부 조인을 할 수 있다.

 

조인 - on 절

 

조인 대상 필터링

    /**
     * 회원과 팀을 조인하면서, 팀 이름이 teamA 인 팀만 조인, 회원은 모두 조회
     * JPQL : select m, t from Member m left join m.team t on t.name = 'teamA'
     */
    @Test
    public void join_on_filtering(){
        List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .leftJoin(member.team, team).on(team.name.eq("teamA")) // member는 다 가져오고 team은 teamA 인 것만
                .fetch();
        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
        }
    }

 

결과

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

 

on 절을 활용해 조인 대상을 필터링할 때, 외부 조인이 아니라 내부 조인을 하면

where 절에서 필터링 하는 것과 기능이 동일하다. 익숙한 where 절을 사용하고, 정말 외부 조인이 필요한 경우에만 위처럼 하면 된다.

 

inner join

       List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .join(member.team, team)
                .where(team.name.eq("teamA")) //.on(team.name.eq("teamA")) 과 같다. 
                .fetch();
        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
        }

 

결과

tuple = [Member(id=1, username=member1, age=10), Team(id=1, name=teamA)]
tuple = [Member(id=2, username=member2, age=20), Team(id=1, name=teamA)]

 

 

연관관계까 없는 엔티티 외부 조인

/**
*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(){
        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))
                .fetch();
      
         
        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
        }
    }
tuple = [Member(id=1, username=member1, age=10), null]
tuple = [Member(id=2, username=member2, age=20), null]
tuple = [Member(id=3, username=member3, age=30), null]
tuple = [Member(id=4, username=member4, age=40), null]
tuple = [Member(id=5, username=teamA, age=0), Team(id=1, name=teamA)]
tuple = [Member(id=6, username=teamB, age=0), Team(id=2, name=teamB)]
tuple = [Member(id=7, username=teamC, age=0), null]

 

모든 member를 조회하되, member 이름과 team 의 이름이 같은 경우에는 팀을 가지고 온다.

leftJoin(member.team, team) 하면 조인 on 절에 id가 들어간다.

leftJoin(member.team, team)을 빼버리면 id로 매칭 안하고 on절에 있는것처럼 이름으로만 조인이 된다.

 

일반조인 : leftJoin(member.team, team)

on 조인: from(member).leftJoin(team).on(xxx)

 

조인 - 페치 조인

즉시 로딩으로 Member, Team SQL 쿼리 조인으로 한번에 조회 

  	@PersistenceUnit
    EntityManagerFactory emf;
    
    @Test
    public 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();

    }

 

서브 쿼리

 /**
     * 나이가 가장 많은 회원 조회
     */
    @Test
    public void subQuery(){
        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);
    }

    /**
     * 나이가 평균 이상인 회원 조회
     */
    @Test
    public 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);
    }

    /**
     * 나이가 10이상인 회원들 조회
     */
    @Test
    public 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);
    }

			// select 절에 sub query
    @Test
    public void selectSubQuery(){
        QMember memberSub = new QMember("memberSub");

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

 

from 절의 서브쿼리 한계 

PA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다.

당연히 Querydsl도 지원하지않는다. 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다. Querydsl도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.

 

from 절의 서브쿼리 해결방안

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

 

Case 문

    @Test
    public void basicCase(){
        List<String> result = queryFactory
                .select(member.age
                        .when(10).then("열살")
                        .when(20).then("스무살")
                        .otherwise("기타"))
                .from(member)
                .fetch();
        // 실무에서는 이렇게 DB에서 전환하는걸 하지 않는 것을 권장한다. 데이터만 가져온다. 애플리케이션이나 프레젠테이션 레이어에서 해결한다. 

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

 

상수, 문자 더하기

    @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);
        }
    }
tuple = [member1, A]
tuple = [member2, A]
tuple = [member3, A]
tuple = [member4, A]

 

 

    @Test
    public void concat(){
        List<String> result = queryFactory
                .select(member.username.concat("_").concat(member.age.stringValue())) //stringValue 는 enum을 처리할 때도 많이 사용
                .from(member)
                .where(member.username.eq("member1"))
                .fetch();

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

 

 

 

 

 

인프런 - 실전! Querydsl

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