본문 바로가기
데이터 접근 기술

Querydsl 기본(3)

by oncerun 2023. 2. 7.
반응형

Lazy의 연관관계를 fetch로 가져오는 방법을 Querydsl을 활용해 가져와보자.

fetch join은 성능 최적화를 위해 연관된 엔티티를 동시에 조회하기 위해 많이 사용한다.
하지만 일대다 관계가 많아질수록 이는 행의 수가 중복돼서 생성되기 때문에 Set이나 distinct를 통한 중복을 제거해 주는 별도의 방법이 필요했다.

보통 일대다 관계가 연속적으로 필요한 경우 batch size를 통해 성능 최적화를 진행했다.

나는 여기서 한 가지 궁금증이 생겼다. fetch join을 사용할 때 만약 연관관계에 있는 특정 필드를 꺼내서 전달하려는 목적을 가졌을 때는 Projections을 최대한으로 활용하고 연관관계를 통해 변경을 가하도록 영향을 주는 비즈니스 로직이 있을 때 fetch join을 사용하도록 두 Layer을 분리하는 건 어떨까?

잠시 고민은 넣어두고 다시 Querydsl fetch join의 기초를 잡아보자.

Member와 Team은 N:1 관계를 가진다.

이때 Member를 fetch join 없이 조회 시 Team은 Lazy 로딩으로 proxy 객체가 존재해야 한다.

이를 증명하는 건 다음과 같다.

@PersistenceUnit
EntityManagerFactory emf;

@Test
public void noFetchJoin() throws Exception {

    em.flush();
    em.clear();

    Member result = queryFactory
            .selectFrom(member)
            .where(member.username.eq("member1"))
            .fetchOne();


    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(result.getTeam());
    assertThat(loaded).isFalse();
}


이는 fetchjoin을 적용하지 않았기 때문에 엔티티의 로딩 상태가 false이다.

반대로 fetch join을 사용해 보자.

@Test
public void fetchJoin() throws Exception {

    em.flush();
    em.clear();

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


    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(result.getTeam());
    assertThat(loaded).isTrue();
}


문법은 fetchJoin()을 붙여주면 된다. 이때 별도로 받는 파라미터는 없다.


서브 쿼리


com.querydsl.jpa.JPAExpressions 사용하여 사용할 수 있다.

@Test
public void subQuery() throws Exception {
    QMember memberSub = new QMember("memberSub");

    Member result = queryFactory
            .selectFrom(member)
            .where(member.age.eq(
                    JPAExpressions.select(memberSub.age.max())
                            .from(memberSub)
            ))
            .fetchOne();

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


서브쿼리의 alias는 달라야 하기 때문에 별도의 Q Type을 생성하여 별칭으로 사용한다.

실제 발생되는 쿼리는 다음과 같다.

    select
        member0_."member_id" as member_i1_0_,
        member0_."age" as age2_0_,
        member0_."team_id" as team_id4_0_,
        member0_."username" as username3_0_ 
    from
        "member" member0_ 
    where
        member0_."age"=(
            select
                max(member1_."age") 
            from
                "member" member1_
        )


그렇다면 JPAExpressions를 활용해 인라인뷰나 스칼라 서브쿼리도 사용이 가능할 것 같다.

@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(member.age.gt(10))
            ))
            .fetch();

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


찾아보니 jpql에서 인라인뷰를 지원하지 않기 때문에 Querydsl도 인라인뷰를 지원하지 않는다고 한다.
다만 하이버네이트 구현체를 사용하면 스칼라 서브쿼리는 지원한다고 한다.

만약 서브쿼리를 join으로 변경할 수 있다면 이를 통해 인라인뷰를 지원할 수 있다.
만약 변경이 불가능하다면 nativeSQL을 사용하는 방안으로 가자.
혹은 애플리케이션에서 쿼리를 분리해서 실행할 수 있다.

여기에 함정이 있는지 확인해 보자.

인라인 뷰를 사용하는 것이 과연 올바른가에 대해 의문을 가질 필요가 있다는 것이다.

SQL AntiPatterns라는 책을 참고해 보자.




case 문

case문은 언제 사용하게 될까?

. select 절에서 데이터를 가공하는 경우 보통 화면에 알맞은 데이터를 뿌려주거나, 애플리케이션에서 분기하여 처리하는 것보다 데이터베이스 자원을 사용하는 것이 더 성능상 이점이 있을 때 사용할 것으로 예상된다.

문법은 다음과 같다.

List<String> result = queryFactory
        .select(member.age
                .when(10).then("10살")
                .when(20).then("20살")
                .otherwise("others"))
        .from(member)
        .fetch();
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("others"))
        .from(member)
        .fetch();


복잡한 경우 CaseBuilder()를 사용할 수 있다.

하지만 과연 데이터 가공이 데이터베이스를 이용하여 얻는 것이 올바른지 생각해보아야 한다.

상수, 문자 더하기.

이는 생각보다 필요할 때가 많았던 것 같다.

이는 Expressions.constant라는 메서드를 통해 생성할 수 있다.

또한 prepend, append 함수를 통해 문자열에 다음 문자열을 쉽게 붙일 수 있지만 만약 String 타입이 아닌 경우에는 String 타입으로 변환을 해주어야 한다.

queryFactory
        .select(member.username, Expressions.constant("A"))
        .from(member)
        .fetch();

queryFactory
        .select(member.username.concat("_").concat(member.age.stringValue()))
        .from(member)
        .where(member.username.eq("member1"))
        .fetchOne();


다만 첫 번째로 시도한 constant 같은 경우는 SQL 로그에 남지 않지만 실제 결과는 다음과 같다.

    select
        member0_."username" as col_0_0_ 
    from
        "member" member0_
        
select member1.username
from Member member1, time: 3ms, rows: 4

[member1, A]
[member2, A]
[member3, A]
[member4, A]


두 번째는 다음과 같이 타입 캐스팅 문이 같이 나간다.

    select
        ((member0_."username"||?)||cast(member0_."age" as character varying)) as col_0_0_ 
    from
        "member" member0_ 
    where
        member0_."username"=?


간단하게 문자열을 앞뒤로 붙이기 위해선 append와 prepend를 사용해 보자.

List<String> result = queryFactory
        .select(member.username.prepend("이름 : ").append("입니다."))
        .from(member)
        .fetch();
    select
        ((?||member0_."username")||?) as col_0_0_ 
    from
        "member" member0_
2023-02-07 23:40:08.741 TRACE 7588 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [이름 : ]
2023-02-07 23:40:08.741 TRACE 7588 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [입니다.]
2023-02-07 23:40:08.741 DEBUG 7588 --- [           main] o.h.stat.internal.StatisticsImpl         : HHH000117: HQL: select concat(concat(?1,member1.username),?2)
from Member member1, time: 1ms, rows: 4
이름 : member1입니다.
이름 : member2입니다.
이름 : member3입니다.
이름 : member4입니다.


반응형

'데이터 접근 기술' 카테고리의 다른 글

Querydsl Projections  (0) 2023.02.09
OneToOne 연관관계에 대한 고민.  (0) 2023.02.09
Querydsl 기본(2)  (0) 2023.02.06
QueryDSL 적용 방법 알아보기.  (0) 2023.02.06
[Querydsl] 기본  (0) 2023.02.01

댓글