반응형

 

 

이전 포스팅에서 기본적인 QueryDSL의 검색 쿼리 문법에 대해서 살펴보았었다. 이번 포스팅에서는 연관된 다른 릴레이션과의 연산을 수행하는 Join과 Subquery 방법에 대해서 알아보도록 하겠다.

 

아래 포스팅은 QueryDSL의 기본 검색에 대한 문법을 정리해 둔 것이니 참고하면 좋을 것 같다.

 

https://sjh9708.tistory.com/175

 

[Spring Boot/JPA] QueryDSL 문법(1) : 기본 검색 (선택, 조건, 정렬, 집계, 그룹화)

이전 포스팅에서 QueryDSL 사용을 Repository에서 할 수 있도록 설정하는 방법에 대해서 다루어 보았었다. 이제 실제로 자주 사용되는 SQL문을 QueryDSL을 통해 작성해보도록 하자. JpaRepository는 인터페이

sjh9708.tistory.com

 

 


 

사용할 데이터

 

 

1. Author : Book = 1 : N

Author(저자)는 여러 개의 Book(책)을 가진다.

 

2. Author : Organization = N : 1

Author(저자)는 한곳의 Organization(조직)에 속한다.

 

3. Book : Review = 1 : N

Book(책)은 여러 개의 Review(리뷰)를 가진다.

 

@Entity
@Table(name = "Organization")
@Getter
@Setter
public class Organization {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String orgName;

    @OneToMany(mappedBy = "organization", cascade = CascadeType.ALL)
    private List<Author> authors = new ArrayList<>();

}
@Entity
@Table(name = "Author")
@Getter
@Setter
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
    private List<Book> book = new ArrayList<>();

    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "organization_id")
    private Organization organization;

}
@Entity
@Table(name = "Book")
@Getter
@Setter
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "author_id")
    private Author author;

    @OneToMany(mappedBy = "book", cascade = CascadeType.ALL)
    private List<Review> reviews = new ArrayList<>();

}
@Entity
@Table(name = "Review")
@Getter
@Setter
public class Review {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String comment;

    @ManyToOne
    @JoinColumn(name = "book_id")
    private Book book;

}

 

 

 

 

 


서브 쿼리

 

다음 코드는 나이가 가장 많은 저자를 조회하는 서브쿼리를 QueryDSL로 작성한 내용이다.

import com.querydsl.jpa.JPAExpressions;

//...

@Test
public void subQuery(){

    QAuthor subAuthor = new QAuthor("subAuthor");
    Author result = queryFactory
            .selectFrom(author)
            .where(author.age.eq(
                    JPAExpressions
                            .select(subAuthor.age.max())
                            .from(subAuthor)
            ))
            .fetchFirst();

    System.out.println("Sub Query : " + result);

}

 

다음 코드에서 두 가지 특징이 있다.

 

1. 별칭 사용

subAuthor이라는 별칭을 지정하여 QType을 새로 생성했다.

해당 쿼리는 크게 바깥쪽의 쿼리와, Where문 안쪽에서 실행되는 쿼리로 나눌 수 있다. 따라서 이들은 독립적인 쿼리이고, 별칭은 메인 쿼리의 author와 서브쿼리의 subAuthor를 구별할 수 있게 하기 위함이다.

QueryDSL은 이를 바탕으로 투명하게 서브쿼리의 결과를 메인 쿼리와 연관지을 수 있다.

 

2. JPAExpression

Hibernate JPA의 JPAExpressions를 사용하여 QueryDSL에서 서브쿼리를 생성하는 데 사용할 수 있다.

 

 

 

import static com.querydsl.jpa.JPAExpressions.*;

@Test
public void subQuery(){

    QAuthor subAuthor = new QAuthor("subAuthor");
    Author result = queryFactory
            .selectFrom(author)
            .where(author.age.eq(
                    select(subAuthor.age.max())
                            .from(subAuthor)
            ))
            .fetchFirst();

    System.out.println("Sub Query : " + result);

}

 

JPAExpression을 Static Import하여 다음과 같이 문법을 간소화 시킬 수도 있다.

Where 안쪽의 Select절은 jpa.JPAExpressions 패키지의 것이고, 바깥의 Select절은 JPAQueryFactory의 것이다.

 

 

 

 

2024-02-01T00:52:09.227+09:00 DEBUG 76908 --- [    Test worker] org.hibernate.SQL                        : 
    select
        a1_0.id,
        a1_0.age,
        a1_0.gender,
        a1_0.name,
        a1_0.organization_id 
    from
        author a1_0 
    where
        a1_0.age=(
            select
                max(a2_0.age) 
            from
                author a2_0
        ) 
    limit
        ?
Sub Query : Author(id=2, name=Jane Smith, age=40, gender=F)

 

다음과 같이 서브 쿼리의 결과를 확인할 수 있다.

 

 

 

 

 

 


Where절 서브쿼리 안에서의 Join문

 

@Test
public void subQuery2(){

    QAuthor subAuthor = new QAuthor("subAuthor");
    Author result = queryFactory
            .selectFrom(author)
            .where(author.book.size().goe(
                    select(subAuthor.book.size().avg())
                            .from(subAuthor)
                            .innerJoin(subAuthor.book, book)
            ))
            .fetchFirst();

    System.out.println("Sub Query : " + result);

}

 

 저자들이 각각 서술한 책이 모든 저자들의 서술한 책의 평균보다 많은 저자를 조회하는 쿼리이다.

SQL과 마찬가지로 서브쿼리 안에서 Join문이 가능하며, 위와 같은 형태로 사용하면 된다.

 

 

2024-02-01T00:52:09.672+09:00 DEBUG 76908 --- [    Test worker] org.hibernate.SQL                        : 
    select
        a1_0.id,
        a1_0.age,
        a1_0.gender,
        a1_0.name,
        a1_0.organization_id 
    from
        author a1_0 
    where
        (
            select
                count(1) 
            from
                book b1_0 
            where
                a1_0.id=b1_0.author_id
        )>=(
            select
                avg((select
                    count(1) 
                from
                    book b3_0 
                where
                    a2_0.id=b3_0.author_id)) 
            from
                author a2_0 
            join
                book b2_0 
                    on a2_0.id=b2_0.author_id) 
        limit
            ?
Sub Query : Author(id=1, name=John Doe, age=25, gender=M)

 

 

 

 


Select 절 안에서의 서브쿼리

 

@Test
public void subQuery3(){

    QAuthor subAuthor = new QAuthor("subAuthor");
    List<Tuple> result = queryFactory
            .select(author.name,
                    select(subAuthor.age.avg())
                            .from(subAuthor)
            )
            .from(author)
            .fetch();

    print("Sub Query : ", result);

}

 

이번에는 Select 절에서 서브쿼리를 사용하여 작성한 예시이다.  저자들의 이름과 함께 모든 저자들의 나이의 평균을 함께 선택하는 예시 쿼리이다.

굳이 서브쿼리를 사용할 필요는 없었겠지만 예시를 위해서 작성해보았다.

 

 

 

 

 


 

From 절 안에서의 서브쿼리

 

결론부터 말하면 불가능하다.

JPA 표준에서 하위 쿼리의 FROM 절에서는 엔터티나 엔터티 경로만 사용할 수 있으며 서브쿼리를 지원하지 않는다.

QueryDSL은 태생적으로 JPA에서 발전된 기술이기 때문에 기존의 이러한 제약을 따른다.

 

 

 

 

 


상수와 문자열 조작

 

 

쿼리를 작성하다가 보면 상수가 필요한 순간, 혹은 쿼리 결과로 나온 문자열을 가공하여 사용해야 하는 순간들이 많다.

와닿지 않는다면 우선 아래의 쿼리를 살펴보자

 

@Test
public void JoinWithConcat() throws Exception {
    //given

    //when
    List<Tuple> results = queryFactory
            .select(book, review)
            .from(book)
            .innerJoin(review)
            .on(review.comment.like(book.title.prepend("%").concat("%")))
            .fetch();

    print("JoinWithConcat", results);

    //then

}

 

해당 쿼리는 Book과 연관된 Review들을 선택하는데, 기본적인 Join, 즉 외래키로 연관된 Review들이 아닌 해당 책의 제목이 포함된 리뷰들이라면 연관관계에 상관없이 Join해서 가져오는 쿼리를 작성한 것이다.

 

이 때 ON 절에서 LIKE 연산 시, Book의 Title에 와일드카드(%)를 추가하여 쿼리를 수행해야 한다.

메타모델에서 prepend()는 앞쪽에 문자열을 추가하고, concat()는 뒤쪽에 문자열을 추가할 수 있는 메서드가 있으며, 이를 통해 문자열을 조작하여 새로 쿼리에 사용할 수 있게 되는 것이다.

 

 

 

 

 

@Test
public void SelectWithConcatAndExression() throws Exception {
    //given

    //when
    List<Tuple> results = queryFactory
            .select(author.name.concat("_").concat(author.age.stringValue()),
                    author.age.add(10),
                    Expressions.constant("ABC"),
                    Expressions.constant(new Date().toString()))
            .from(author)
            .fetch();

    print("SelectWithConcatAndExression", results);

    //then

}

 

이번 예시에서는 총 4개의 컬럼을 Select하고 있다.

1번째 컬럼 : Name + "_" + Age를 문자열로 이어서 반환, Age는 Integer이므로 stringValue()를 통해 형변환 하여 사용

2번째 컬럼 : Age에 정수 10을 더함

3번째 컬럼 : "ABC"라는 상수

4번째 컬럼 : 현재 Timestamp

 

 

 

 


CASE문

 

@Test
public void caseSelect(){

    List<Tuple> result = queryFactory
            .select(author.name,
                    author.gender
                            .when("M").then("Male")
                            .otherwise("Female"),
                    new CaseBuilder()
                        .when(author.age.between(0, 35)).then("Junior")
                        .otherwise("Senior")
            )
            .from(author)
            .fetch();

    print("caseSelect : ", result);

}

 

SQL의 Case문을 사용할 수 있다. 이번 쿼리에서는 총 세 개의 컬럼을 Select하는데 2번째와 3번째 컬럼의 기능은 아래와 같다.

 

2번째 컬럼 : gender 컬럼이 "M"일 경우 "Male", 그 외일 경우 "Female"로 반환

3번째 컬럼 : age가 0세에서 35세 사이일 경우 "Junior", 그 외일 경우 "Senior"로 반환. 다음과 같이 Equal 연산 이외의 복잡한 연산이 필요할 때에는 CaseBuilder()를 사용한다.

 

 

 

 

 

@Test
public void caseSelect2(){

    StringExpression career = new CaseBuilder()
            .when(author.age.between(0, 35)).then("Junior")
            .otherwise("Senior");

    List<Tuple> result = queryFactory
            .select(author.name,
                    author.gender
                            .when("M").then("Male")
                            .otherwise("Female"),
                    career

            )
            .from(author)
            .orderBy(career.desc())
            .fetch();

    print("caseSelect2 : ", result);

}

 

위에서 작성했던 복잡한 CaseBuilder()의 조건을 따로 빼서 변수를 생성하여 사용할 수도 있다.

Select절에서 변수 형태로 사용했을 뿐 만 아니라, OrderBy 절을 비롯한 다른 부분에서도 활용하는 방식으로 사용할 수 있다.

 

 

 

 

 


 

함께 보기

 

MySQL의 서브쿼리

https://sjh9708.tistory.com/177

 

[MySQL] SQL 문법 정리 (9) : Subquery (서브쿼리)

사용 데이터 Subquery (서브쿼리) SQL문 안에서 또 다른 SQL문을 사용하는 방법을 의미한다. 외부 쿼리의 결과에 따라 조건을 동적으로 결정할 수 있다. 서브쿼리를 통해 계산된 값을 사용하여 외부

sjh9708.tistory.com

 

 


 

함께 보기

 

1. QueryDSL 설정과 Repository에서의 사용

https://sjh9708.tistory.com/174

 

[Spring Boot/JPA] QueryDSL 설정과 Repository에서의 사용

이번 포스팅에서는 Spring Boot에서 이전에 사용했던 JPQL와 JpaRepository 보다 조금 더 객체지향스럽고 유동적인 동적 쿼리를 작성할 수 있도록 QueryDSL 사용을 위한 설정을 해보도록 하자. QueryDSL에 사

sjh9708.tistory.com

 

2. QueryDSL 기본 문법 

https://sjh9708.tistory.com/175

 

[Spring Boot/JPA] QueryDSL 문법(1) : 기본 검색 (선택, 조건, 정렬, 집계, 그룹화)

이전 포스팅에서 QueryDSL 사용을 Repository에서 할 수 있도록 설정하는 방법에 대해서 다루어 보았었다. 이제 실제로 자주 사용되는 SQL문을 QueryDSL을 통해 작성해보도록 하자. JpaRepository는 인터페이

sjh9708.tistory.com


3. QueryDSL Join 문법

https://sjh9708.tistory.com/178

 

[Spring Boot/JPA] QueryDSL 문법(2) : Join 표현식

이전 포스팅에서 기본적인 QueryDSL의 검색 쿼리 문법에 대해서 살펴보았었다. 이번 포스팅에서는 연관된 다른 릴레이션과의 연산을 수행하는 Join과 Subquery 방법에 대해서 알아보도록 하겠다. 아

sjh9708.tistory.com


4. QueryDSL 서브쿼리와 상수/문자열 조작

https://sjh9708.tistory.com/180

 

[Spring Boot/JPA] QueryDSL 문법(3) : 서브쿼리, 상수/문자열 조작

이전 포스팅에서 기본적인 QueryDSL의 검색 쿼리 문법에 대해서 살펴보았었다. 이번 포스팅에서는 연관된 다른 릴레이션과의 연산을 수행하는 Join과 Subquery 방법에 대해서 알아보도록 하겠다. 아

sjh9708.tistory.com

 

5.  QueryDSL 프로젝션과 Entity > DTO 변환 방법들

https://sjh9708.tistory.com/181

 

[Spring Boot/JPA] Entity > DTO 변환 방법들 및 QueryDSL 프로젝션

이전 포스팅들에서 JPQL, Spring Data JPA Repository, QueryDSL 등을 통해서 데이터를 조회하는 방법들에 대해서 다루어 보았었다. 이번에는 쿼리 결과로 나온 Entity 혹은 Tuple들을 DTO로 매칭하는 방법을 알

sjh9708.tistory.com

 

6.  QueryDSL 동적 쿼리 작성하기

https://sjh9708.tistory.com/182

 

[Spring Boot/JPA] QueryDSL : 동적 쿼리 작성하기

JPA를 사용할 때, QueryDSL을 도입하는 가장 큰 이유는 동적 쿼리 작성에 매우 유연하다는 점이다. 이번 포스팅에서는 동적 쿼리를 작성하는 방법에 대해서 알아보도록 하겠다. 사용할 데이터 1. Auth

sjh9708.tistory.com

 

반응형

BELATED ARTICLES

more