Programming/Querydsl

Querydsl - 기본 문법 (3) (조인, 서브 쿼리, Case문, 상수, 문자 더하기)

잇(IT) 2023. 7. 27. 14:33
- 조인 - 기본 조인
/*
    * 팀 A에 소속된 모든 회원
    * */
    @Test
    public 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");
    }

- join(member.team, team)을 통해 join을 할 수 있다. join만 작성하게 되면 inner join을 기본으로 한다.


- 조인 - 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"))
                .join(member.team, team)
//                .on(team.name.eq("teamA")) // where절을 사용하는 것과 동일하다.
                .where(team.name.eq("teamA"))
                .fetch();

        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
        }
    }
                .leftJoin(member.team, team).on(team.name.eq("teamA"))
                
                .join(member.team, team)

- leftJoin의 경우 주체가 되는 엔티티의 모든 member 정보를 전부 불러오지만, inner join의 경우 두 엔티티가 전부 부합하는 데이터에 대해서만 결과 값을 가져온다.

 

- left Join

- member 테이블에 있는 총 4개의 엔티티를 가져오는 것을 확인할 수 있다.

 

- inner Join

- 추가로 inner join을 사용할 때는 on 대신 where을 사용할 수 있다.

- where을 사용하든 on을 사용하든 결과는 동일하다.


- 세타 조인

- 세타 조인이란 연관관계가 없는 필드로 조인하는 것을 뜻한다.

/*
     * 연관관계가 없는 엔티티 외부 조인
     * 회원의 이름이 팀 이름과 같은 대상 외부 조인
     * */
    @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))
                //join 방식이 다르다. member.team, team과 같이 join하는 것이 아니다.
                .fetch();

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

- select에서 member와 team을 같이 조회한다.

- 일반적으로 left Join을 하게 될 경우 (member.team, team)과 같이 연관된 데이터끼리 묶어 join을 한다.

- 하지만 세타 조인의 경우 leftJoin(team)과 같이 from에 연관된 엔티티와 관련 없는 엔티티를 left Join 값으로 넣는다.

- 세타 조인을 할 경우 모든 경우 수에 대해 DB가 전부 join을 하게 된다.

- on을 통해 두 테이블에서 조건에 만족하는 값을 결과로 가져온다.


- 페치 조인

 

- LAZY 지연 로딩이 적용되지 않은 경우

@PersistenceUnit
    EntityManagerFactory emf;

    @Test
    public 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();
        // Team의 경우 LAZY이고 fetch join을 적용하지 않았기 때문에
        // 당연히 team에 대한 fetch 조인 및 조회는 발생하지 않는다.
    }

- Member, Team 연관관계에서 Team의 경우 LAZY 즉, 지연 로딩으로 설정되어 있기 때문에 Team 속성에 대해 조회하지 않으면 Team에 대한 쿼리를 날리지 않는다.

 

boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).as("페치 조인 미적용").isFalse();

- 해당 코드는 페치 조인이 올바르게 적용되었는지 확인하는 코드이다.

- 페치 조인이 적용되지 않았기 때문에 False로 테스트 했을 때 정상적으로 통과한다.


- 페치 조인 적용

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

- join() 뒤에 .fetch()를 붙이면 fetch join이 적용된다. 


- 서브 쿼리

 

- JPAExpressions를 통해 서브 쿼리를 작성한다.

- JPA, JPQL의 한계점은 from 절의 서브쿼리를 지원하지 않기 때문에 Querydsl도 마찬가지로 from 절의 서브쿼리를 지원하지 않는다.

 

/*
    * 나이가 가장 많은 회원 조회
    * */
    @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);
    }

- 메인 쿼리와 서브 쿼리의 별칭이 겹치면 안되기 때문에

new QMember("memberSub")

와 같이 새롭게 QMember 객체를 생성해주고 사용해야 한다.

 

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

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

@Test
    public void selectSubQuery() {

        QMember memberSub = new QMember("memberSub");

        List<Tuple> result = queryFactory
                .select(member.username,
                        JPAExpressions //static import가 가능하다.
                                .select(memberSub.age.avg())
                                .from(memberSub)
                )
                .from(member)
                .fetch();

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


- Case문
@Test
    public void basicCase() {
        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);
        }
    }

- .when / .otherwise를 통해 Case문을 작성 할 수 있다.

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

- Expressions.contant를 통해 원하는 상수 값을 출력 데이터에 추가할 수 있다.

- Expressions.constant를 사용하면 JPQL에서는 관련 쿼리가 나가지 않고, 결과에 대해서만 해당 결과를 포함시켜서 출력한다.

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

- concat을 사용하여 결과값을 붙여서 출력하는 경우가 자주 발생한다.

- 반환타입이 일치하지 않으면 concat을 통해 이어붙이는 것이 불가능하다.

- age의 경우 String 타입이 아니기 때문에 stringValue()를 통해 String 타입으로 변환 시켜서 이어 붙여준다.

- enum(열거형) 사용할 때 stringValue()를 많이 사용한다.

 

 

 

 

 

 

 

 

 

 

 

출처 : 인프런 - 김영한(실전! Querydsl)

728x90