RE-Heat 개발자 일지

[Querydsl] [3] 기본 문법 (하편) 본문

백엔드/Querydsl

[Querydsl] [3] 기본 문법 (하편)

RE-Heat 2023. 9. 17. 23:55
 

실전! Querydsl - 인프런 | 강의

Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!, 복잡한 쿼리, 동적 쿼리는 이제 안녕! Querydsl로 자바 백엔드 기술을 단단하게. 🚩 본 강의는 로드맵 과정입니다. 본 강의는 자바 백엔

www.inflearn.com

인프런 김영한 님의 강의를 듣고 작성한 글입니다.

 

[8] 조인 - 기본 조인

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

 

■ 조인 종류

  • join() , innerJoin() : 내부 조인(inner join) 
  • leftJoin() : left 외부 조인(left outer join) 
  • rightJoin() : rigth 외부조인(rigth outer join)

 

■ 기본 조인

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

    assertThat(result)
            .extracting("username")
            .containsExactly("member1", "member2");
}
  • member.team 즉 team 엔티티와 조인.
  • teamA인 데이터만 추출하도록 작성
  • username필드로 추출하고 예상 값이 있는지 containsExactly 메서드로 확인
    • 참고] isEqualTo를 쓰면 추출한 값이 [] 안에 있어 테스트 실패가 나온다.

 

■ 세타 조인

/**
 * 세타 조인
 * 회원의 이름이 팀 이름과 같은 회원 조회
 */
@Test
public void theta_join() throws Exception {
    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 절에서 여러 엔티티를 선택해 세타 조인한다. (member, team)
  • 연관관계가 설정되지 않아도 join이 가능하다.
  • 카테시안 조인 (N * M)

  • SQL에 cross join(상호 조인)이 명시돼 있다. 참고로 상호 조인은 한쪽 테이블의 모든 행과 다른 쪽 테이블의 모든 행을 조인시키는 기능이다.


[9] 조인 - on절

■ 조인 대상 필터링

/**
 * 예시) 회원과 팀을 조인하면서 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
 * JPQL : select m from Member m left join 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)  
            .where(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]
  • 레프트 조인이므로 member 데이터는 우선 다 가져온다.
  • 그러나 team은 조건이 맞지 않는 데이터는 null 상태로 가져온다.

 

참고] 당연히 inner조인은 조건을 충족하는 데이터만 가져온다.

@Test
public void join_on_filtering() throws Exception {
    List<Tuple> result = queryFactory
            .select(member, team)
            .from(member)
            .join(member.team, team)
            //.on(team.name.eq("teamA"))
            .where(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)]
  • inner 조인은 on절이나 where절이나 결과는 동일하다.
영한님 : 내부조인이면 익숙한 where절로 해결하고 외부 조인이 필요할 때만 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))
            .fetch();

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

 

※ 주의사항

  • 일반조인 : leftJoin(member.team, team)
  • on 조인 : from(member).leftJoin(team).on(xxx)

연관관계가 없으므로 member.team이 아닌 team으로 가져온 것에 유의하자.


[10] 조인 - 페치 조인

SQL 조인을 활용해 연관된 엔티티를 SQL 한 번으로 조회하는 기능이다. N + 1 문제를 해결할 때 자주 쓰인다.

 

■ 페치 조인 미적용

@PersistenceUnit
EntityManagerFactory emf;

@Test
public void fetchJoinNo() throws Exception {
    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을 따로 조회하지 않았으므로 team이 비었다(프록시).
  • 그래서 loaded가 false로 나온다.

 

■ 페치 조인 적용

@Test
public void fetchJoinUse() throws Exception {
    em.flush();
    em.clear();

    Member findMember = queryFactory
            .selectFrom(member)
            .where(member.username.eq("member1"))
            .join(member.team, team).fetchJoin()
            .fetchOne();

    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertThat(loaded).as("페치 조인 미적용").isTrue();
}
  • 페치조인을 적용하려면 join뒤에 .fetchJoin()을 추가하면 된다.
  • 페치조인으로 한 번에 가져왔으므로 team 데이터도 가져왔다. 그래서 loaded가 true값이 나온다.

페치 조인 관련 게시글 링크

 

[11] 서브 쿼리

쿼리 안에 쿼리 넣기! com.querydsl.jpa.JPAExpressions를 사용

 

■ 서브쿼리 eq 사용

/**
 * 나이가 가장 많은 회원 조회
 */
@Test
public void subQuery() throws Exception {
    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);
}
  • 서브쿼리를 쓸 때 기존 alias와는 달라야 하는 것처럼 여기서도 따로 QMember memberSub = new QMember("memberSub")를 만들었다.
  • JPAExpressions를 쓰고 기존처럼 만들면 된다.

 

■ 서브쿼리 goe(>=) 사용

/**
 * 나이가 평균 이상인 회원 조회
 */
@Test
public void subQueryGoe() throws Exception {
    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절 사용

/**
 * 나이가 10살보다 많은 회원 조회
 */
@Test
public void subQueryIn() throws Exception {
    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절에 서브쿼리

@Test
public void selectSubQuery() throws Exception {
    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절 서브쿼리 한계

JPA JPQL은 from절 서브쿼리(인라인 뷰)는 지원하지 않는다. 따라서 Querydsl도 이를 지원하지 않는다. 

 

해결방안

① 서브쿼리를 join으로 변경한다.

② 애플리케이션에서 쿼리를 2번 분리해서 실행한다.

③ nativeSQL을 사용한다.

 

[12] Case 문

■ select, 조건절(where), order 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);
    }
}
S = 열살
S = 스무살
S = 기타
S = 기타
  • when : 조건문
  • then : when절이 true일 때 실행
  • otherwise : when절이 false일 때 실행

② 복잡한 조건

@Test
public void complexCase() throws Exception {
    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);
    }
}
  • 복잡한 조건일 땐 CaseBuilder()를 사용.

 

■ orderBy에서 Case 문과 함께 사용하기

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

 

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);
}
//결과
username = member4 age = 40 rank = 3
username = member1 age = 10 rank = 2
username = member2 age = 20 rank = 2
username = member3 age = 30 rank = 1
  • Querydsl은 자바 코드로 작성하기 때문에 rankPath처럼 복잡한 조건을 변수로 선언해서 select절, orderBy절과 함께 사용할 수 있다.

 

참고: DB에선 가급적 조건문을 주지 않아야 한다. DB에선 필요한 값만 가져오고 실제 전환하고 바꾸는 건 application 또는 프레젠테이션 레이어에서 해결하는 게 낫다.


[13] 상수, 문자 더하기

■ 상수 더하기 - Expressions.constant(xxx) 사용

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

    for (Tuple tuple : result) {
        System.out.println("tuple = " + tuple);
    }
}
/* select
    member1.username 
from
    Member member1 */ select
        member0_.username as col_0_0_ 
    from
        member member0_ 

// 결과
result = [member1, A]
  • JPQL에서 상수 관련 코드가 없다. 결과에서만 상수가 나타난다.
  • 이유 : 위와 같이 최적화가 가능하면 SQL에 constant 값을 넘기지 않는다. 대신 상수를 더하는 것처럼 최적화가 어려우면 SQL에 constant 값을 넘긴다.

 

■ 문자 더하기 - concat

@Test
public void concat() throws Exception {
    //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 character varying)) as col_0_0_ 
    from
        member member0_ 
    where
        member0_.username=?
  • age가 int이므로 stringValue()로 문자열로 바꿔줘야 한다.
  • SQL을 보면 cast돼 있는데, stringValue()를 썼기 때문이다.
  • 문자열로 바꿔주는 방법은 ENUM을 처리할 때 자주 사용된다.