[Spring Boot/JPA] QueryDSL 문법(1) : 기본 검색 (선택, 조건, 정렬, 집계, 그룹화)
이전 포스팅에서 QueryDSL 사용을 Repository에서 할 수 있도록 설정하는 방법에 대해서 다루어 보았었다. 이제 실제로 자주 사용되는 SQL문을 QueryDSL을 통해 작성해보도록 하자.
JpaRepository는 인터페이스 단에서 기본적인 쿼리를 제공해주지만 동적이고 복잡한 쿼리를 작성이 필요할 때에 QueryDSL 사용을 하는 것이 유리할 수 있다.
아래 포스팅은 Spring Boot 3점대 이상에서 QueryDSL을 설정하고, Repository에서 사용하는 방법을 정리한 글이니 만약 설정방법을 알고 싶다면 참조하면 좋을 것 같다.
https://sjh9708.tistory.com/174
다뤄볼 내용
이번 포스팅에서는 기초적인 SQL에 해당하는 QueryDSL 기능들을 포스팅할 예정들이다. 해당 내용은 아래와 같다. 다른 엔티티와의 연관관계에 대한 서브쿼리 및 Join에 대해서는 다음 포스팅에서 작성해 볼 예정이다.
1. 검색(Select) : 리스트 조회, 단일 조회, 컬럼 선택
2. 조건(Where) : 비교 (=), 조건 함수 (gt, lt, between..), 복합 조건 연산 (AND, OR)
3. 정렬 (Order By)
4. 페이지네이션 (Offset, Limit)
5. 집계함수(Aggregation)
6. 그룹화 (Group By, Having)
사용할 데이터
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;
}
1. 검색(Select)
리스트 조회
public List<Book> findBookList() {
List<Book> result = queryFactory
.selectFrom(book)
.fetch();
return result;
}
단일 조회
public Book findBookByTitle(String title) {
Book result = queryFactory.selectFrom(book)
.where(book.title.eq(title))
.fetchOne();
return result;
}
- selectFrom(QType) : 해당 엔티티에서 모든 컬럼 조회
- fetch() : 리스트를 조회, 데이터가 존재하지 않을 시 빈 리스트를 반환
- fetchOne() : 단일 조회, 결과가 없으면 Null, 결과가 단일이 아닐 시 Exception 발생
- fetchFirst() : 단일 조회, 결과가 여러개여도 맨 처음 레코드 반환
컬럼 선택하여 조회
public List<String> findBookListTitle() {
List<String> result = queryFactory.select(book.title)
.from(book)
.fetch();
return result;
- select(QType.Column1, QType.Column2) : 엔티티의 해당 컬럼(들) 조회
- from(QType) : 조회 대상 엔티티
2. 조건(Where)
public Book findBookByTitle(String title) {
Book result = queryFactory.selectFrom(book)
.where(book.title.eq(title))
.fetchOne();
return result;
}
- where(조건) : 해당 조건과 일치하는 레코드(들)을 반환
다양한 조건 함수
// 동일 여부
author.name.eq("John"); // 일치
author.name.ne("John"); // 일치X
author.name.isNotNull(); // NullX
// 포함
author.age.in(20, 30, 40); // 포함
author.age.notIn(25, 35, 45); // 미포함
// 문자열
author.name.like("J%"); // LIKE : J로 시작
author.name.startsWith("J"); // J로 시작
author.name.contains("Jo"); // J 포함
// 수 비교
author.age.between(25, 35); // 25 ~ 35
author.age.lt(30); // < 30
author.age.loe(30); // <= 30
author.age.gt(30); // > 30
author.age.goe(30); // >= 30
복합 조건 연산
public List<Author> findAuthorByCondition() {
List<Author> result = queryFactory.selectFrom(author)
.where(
author.age.notBetween(20, 30)
.and(author.age.gt(10))
.and(author.age.lt(50))
)
.fetch();
return result;
}
select * from author
where age NOT BETWEEN 20 and 30
and age > 10 and age < 50;
- and(조건) : AND 복합 조건
- or(조건) : OR 복합 조건
public List<Author> findAuthorByCondition2() {
List<Author> result = queryFactory.selectFrom(author)
.where(
(
author.age.notBetween(20, 30)
.and(author.age.gt(10))
.and(author.age.lt(50))
).or(
author.name.like("%John%")
)
)
.fetch();
return result;
}
select * from author
where (age NOT BETWEEN 20 and 30
and age > 10 and age < 50) or
(name LIKE "%John%");
- () 로 조건들을 묶어 표현식 만들 수 있다.
연관된 엔티티의 컬럼을 조건으로 사용
public List<Book> findBooksByAuthorName(String name) {
List<Book> result = queryFactory
.selectFrom(book)
.where(book.author.name.eq(name))
.fetch();
return result;
}
- book.author.name 을 보면 연관된 엔티티의 컬럼을 조건으로 사용할 수 있다. (Book과 Author은 N:1 관계)
- SQL 쿼리문으로는 아래와 같다.
SELECT Book.*
FROM Book
INNER JOIN Author ON Book.author_id = Author.id
WHERE Author.name = 'John Doe';
3. 정렬 (Order By)
public List<Book> findBookListOrderBy() {
List<Book> result = queryFactory
.selectFrom(book)
.orderBy(book.title.desc())
.fetch();
return result;
}
- orderBy(QType.Column.(정렬조건)) : 해당 컬럼을 정렬조건에 따라 정렬 (디폴트는 asc)
- 정렬조건
- desc() : 내림차순
- asc() : 올림차순
public List<Book> findBookListOrderBy() {
List<Book> result = queryFactory
.selectFrom(book)
.orderBy(book.title.desc().nullsLast())
.fetch();
return result;
}
- nullsLast(), nullsFirst() : Null 데이터에 대한 순서를 부여한다(끝, 시작)
4. 페이지네이션 (Offset, Limit)
public List<Book> findBookListPagenation(int offset, int limit) {
List<Book> result = queryFactory
.selectFrom(book)
.offset(offset)
.limit(limit)
.fetch();
return result;
}
- offset(long x) : 0부터 시작하는 결과에 대한 오프셋(시작 위치)
- limit(long x) : 쿼리 결과에 대한 최대치 제한(Limit)
public List<Book> findBookListPagenation(int offset, int limit) {
QueryResults<Book> res = queryFactory
.selectFrom(book)
.offset(offset)
.limit(limit)
.fetchResults();
System.out.println("Total : " + res.getTotal());
System.out.println("Limit : " + res.getLimit());
System.out.println("Offset : " + res.getOffset());
List<Book> result = res.getResults();
return result;
}
- fetchResults : 결과를 가져올 때 페이지네이션 정보를 함께 가져온다. (total, limit, offset) 결과는 getResults()를 호출 시 얻을 수 있다.
5. 집계함수(Aggregation)
그룹 함수와 함께 주로 사용된다.
public List<Tuple> findAuthorAggregation() {
List<Tuple> result = queryFactory
.select(author.count(), author.age.avg())
.from(author)
.fetch();
return result;
}
- count() : 집합의 행 수 계산
- sum() : 합 계산
- avg() : 평균 계산
- max() : 최대 계산
- min() : 최소 계산
6. 그룹화 (Group By, Having)
public List<Tuple> findAuthorGroupByGender() {
List<Tuple> result = queryFactory
.select(author.gender, author.count(), author.age.avg())
.from(author)
.groupBy(author.gender)
.fetch();
return result;
}
SELECT
author.gender,
COUNT(author.id),
AVG(author.age)
FROM
author
GROUP BY
author.gender;
- groupBy(컬럼1, 컬럼2..) : 해당 컬럼을 기준으로 그룹화한다.
public List<Tuple> findAuthorGroupBy() {
List<Tuple> result = queryFactory
.select(author.organization.orgName, author.count(), author.age.avg())
.from(author)
.groupBy(author.organization.id)
.having(author.age.avg().gt(10))
.fetch();
return result;
}
SELECT
author_organization.org_name,
COUNT(author.id),
AVG(author.age)
FROM
author
JOIN
organization AS author_organization ON author.organization_id = author_organization.id
GROUP BY
author.organization_id
HAVING
AVG(author.age) > 10;
- having(조건) : 특정 조건을 만족하는 그룹을 필터링한다.
- 해당 예시는 연관된 엔티티의 컬럼을 기준으로 GroupBy를 수행한 예시이다.
다음 포스팅
QueryDSL을 사용하면 연관된 엔티티의 복잡한 Join 및 서브쿼리 수행에 있어서 빛을 발한다.
다음 포스팅에서는 여러 엔티티간의 관계를 다루는 내용을 작성을 해보겠당.
함께 보기
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
Reference
https://jddng.tistory.com/334#list3
'Backend > Spring' 카테고리의 다른 글
[Spring Boot/JPA] QueryDSL 문법(3) : 서브쿼리, 상수/문자열 조작 (1) | 2024.02.01 |
---|---|
[Spring Boot/JPA] QueryDSL 문법(2) : Join 표현식 (1) | 2024.01.29 |
[Spring Boot/JPA] QueryDSL 설정과 Repository에서의 사용 (2) | 2024.01.23 |
[Spring Boot] Spring Security : JWT Auth (SpringBoot 3 버전) (9) | 2024.01.15 |
[Spring Boot] Swagger API Docs 작성하기 (SpringDoc, SpringBoot 3 버전) (0) | 2024.01.15 |