[Spring Boot/JPA] QueryDSL 문법(3) : 서브쿼리, 상수/문자열 조작
이전 포스팅에서 기본적인 QueryDSL의 검색 쿼리 문법에 대해서 살펴보았었다. 이번 포스팅에서는 연관된 다른 릴레이션과의 연산을 수행하는 Join과 Subquery 방법에 대해서 알아보도록 하겠다.
아래 포스팅은 QueryDSL의 기본 검색에 대한 문법을 정리해 둔 것이니 참고하면 좋을 것 같다.
https://sjh9708.tistory.com/175
사용할 데이터
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
함께 보기
1. QueryDSL 설정과 Repository에서의 사용
https://sjh9708.tistory.com/174
2. QueryDSL 기본 문법
https://sjh9708.tistory.com/175
3. QueryDSL Join 문법
https://sjh9708.tistory.com/178
4. QueryDSL 서브쿼리와 상수/문자열 조작
https://sjh9708.tistory.com/180
5. QueryDSL 프로젝션과 Entity > DTO 변환 방법들
https://sjh9708.tistory.com/181
6. QueryDSL 동적 쿼리 작성하기
https://sjh9708.tistory.com/182
'Backend > Spring' 카테고리의 다른 글
[Spring Boot/JPA] QueryDSL : 동적 쿼리 작성하기 (2) | 2024.02.13 |
---|---|
[Spring Boot/JPA] Entity > DTO 변환 방법들 및 QueryDSL 프로젝션 (1) | 2024.02.13 |
[Spring Boot/JPA] QueryDSL 문법(2) : Join 표현식 (1) | 2024.01.29 |
[Spring Boot/JPA] QueryDSL 문법(1) : 기본 검색 (선택, 조건, 정렬, 집계, 그룹화) (1) | 2024.01.25 |
[Spring Boot/JPA] QueryDSL 설정과 Repository에서의 사용 (2) | 2024.01.23 |