자바 언어로 JPQL을 작성할 수 있게 해주는 오픈소스 프레임워크 querydsl을 프로젝트에 적용시켜 보도록 하겠다.
Sorting 관련 내용은 따로 복잡하게 하지 않았고, 기본적인 내용은 다른 포스트에서 설명한 만큼 이번에는 페이징까지 한꺼번에 처리하도록 하겠다.
2023.03.21 - [JAVA/Spring] - [QueryDSL] QueryDSL이란? + 간단한 실습 << querydsl에 관한 내용과 프로젝트 설정은 이곳을 참고하자.
1. 적용 준비 (build.gradle, configuration)
- gradle 7.6
- JAVA 17
- querydsl 5.0.0
- spring boot 3.0.2
전술한 블로그 링크 문서와 겹치는 설정 내용이다. build.gradle에 dependency를 추가한다.
dependencies {
// QueryDSL 관련 dependencies
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
// build.gradle 파일의 가장 밑에 아래 추가
def querydslDir = "$buildDir/generated/querydsl"
// build할 때 소스코드로 인식할 범위에 querydslDir추가
sourceSets {
main.java.srcDirs += [ querydslDir ]
}
// compile 단계에서 Q클래스 생성 디렉토리 설정
tasks.withType(JavaCompile) {
options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}
clean.doLast {
file(querydslDir).deleteDir()
}
프로젝트를 빌드하고, querydsl에서 쿼리를 작성하는데 쓰이는 클래스인 Q클래스가 생성되었음을 확인한 후 queryFactory 빈을 configuration한다. querydsl의 쿼리가 필요할 때마다 entityManager 객체를 불러오는 방법도 있지만, 번거롭기 때문에 쿼리팩토리 자체를 빈으로 등록했다.
@Configuration
public class QueryDslConfiguration {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
연관 관계 설명때 언급했지만, 현재 프로젝트에서 엔티티간 연관관계는 총 3가지이다.
- user - farm_log : 일대다. 한 명의 유저(user)가 여러개의 농장일기(farm_log)를 가지고 있다.
- user - user : 다대다. 한 명의 유저(user)는 여러 명의 유저(user)을 팔로잉 할 수 있으며, 또한 여러 명의 유저(user)에게 팔로잉 당할 수 있다. 해당 관계는 follow 연관 테이블에 매핑되어 있다.
- user - farm_log : 다대다. 한 명의 유저(user)는 여러 개의 농장일기(farm_log)에 좋아요를 누를 수 있으며, 또한 한 개의 농장일기(farm_log)는 여러 명의 유저에게 좋아요를 받을 수 있다.
ERD)

동적 쿼리 + 페이징 처리로 작성할 쿼리는 4가지이다.
- 홈 화면에서 보여지는 "모든 농장일기 보기" -> 모든 농장일기를 불러오되, 10개씩 페이징 처리를 할 것이다.
- 홈 화면에서 보여지는 "팔로우한 사람들의 농장일기 보기" -> 내가 작성한 일기와 팔로우한 사람들의 일기만 보이게 할 것이다.
- 특정 유저 페이지에서 보여지는 "해당 유저가 작성한 농장일기 보기" -> 유저 상세보기 페이지의 유저가 작성한 일기만 보이게 할 것이다.
- 특정 유저 페이지에서 보여지는 "해당 유저가 좋아요한 농장일기 보기" -> 유저 상세보기 페이지의 유저가 좋아요한 일기만 보이게 할 것이다.
2. Custom PageRequst
스프링 프레임워크에선 paging을 위해 Pageable 인터페이스와 PageRequest 클래스를 지원한다. (PageRequest가 Pageable을 implement하는 구조)
페이징에 필요한 페이지 번호(page), 페이지 사이즈(size), 정렬 정보(sort) 객체를 받아 PageRequest를 구성하여 offset, limit을 가져와 실제 쿼리에 이용하는 구조이다.
페이징에 필요한 페이지 번호는 query parameter로서 받는다. page, size, sort 각각을 아래와 같은 url 링크로 받는다.
/board?page=0&size=20&sort=created_at,desc
spring mvc의 handler method parameter로 PageRequest 혹은 Pageable 객체를 두면, 아래와 같은 방법을 통해 query parameter로 들어온 값을 Pageable 객체로 사용할 수 있다.
import org.springframework.data.domain.PageRequest;
@GetMapping("/pageRequest")
public String pagingExample(PageRequest pageRequest) {
Pageable pageable = pageRequets.of();
// of() static method 통해
// pageable의 offset, pageSize 등 접근 가능
}
그러나 위와 같이 dafault PageRequest 객체를 이용하면 몇 가지 단점이 있다.
- 페이지 시작점이 0번부터라는 점 (1부터 시작하는 것이 직관적으로 좋다)
- 페이지의 사이즈가 임의조절 될 수 있고, 그 최대값이 정해지지 않다는 점
이외에도 기타 예외처리를 하기 어려워 직접 PageRequest 객체를 만들어 사용하는 것이 좋다.
프로젝트에선 페이지 사이즈를 10으로 고정했다.
public interface UtilConst {
int DEFAULT_PAGE_SIZE = 10;
}
그리고 1부터 시작하는 페이지 번호를 기준으로, sort나 pageSize 관련 정보는 받지 않고 페이지 번호만을 받았다. 페이징을 조금 더 심오하게 진행하면 각 필드의 정렬 방향(desc, asc)을 결정하는 항목도 추가할 수 있겠지만, 지금은 단순히 페이지만 나누는 기능을 구현할 목적이므로
public class PageRequest {
private int page = 1;
public void setPage(String page) {
try {
this.page = Math.min(1,Integer.parseInt(page));
} catch (NumberFormatException e) {
this.page = 1;
}
}
public int getPage() {
return page;
}
public org.springframework.data.domain.PageRequest of() {
return org.springframework.data.domain.PageRequest.of(
this.page - 1, UtilConst.DEFAULT_PAGE_SIZE);
}
}
- NumberFormatException e : 쿼리 파라미터로 들어온 page 값이 int가 아닌 예외를 처리해주었다.
- of() : custom PageRequest에서 전처리를 한 PageRequest를 바탕으로 스프링에서 쓰는 PageRequest를 return한다. 페이지 번호의 시작이 0부터이므로 custom PageRequest의 page 번호에 1을 빼줬다.
이제 handler method의 인자에 위 POJO 클래스를 집어넣으면, methodArgumentResolver가 정의한 setPage()를 호출해 실제 스프링 프레임워크에서 지원하는 Pageabl을 사용할 수 있다.
@Controller
@RequiredArgsConstructor
public class MyController {
@GetMapping("/home")
public String getHome(Model model,
final PageRequest pageRequest) {
Pageable pageable = pageRequest.of();
return "index.html";
}
}
3. Custom Repository
queryDsl의 쿼리를 이용할 custom query들을 정의했다. 앞서 언급한 4개의 쿼리들이다.
public interface FarmLogCustomRepository {
PageImpl<FarmLog> findAllFarmLogsQueryDslPaging(Pageable pageable);
PageImpl<FarmLog> findFollowingFarmLogsQueryDslPaging(Pageable pageable, User following);
PageImpl<FarmLog> findByAuthorQueryDslPaging(User author, Pageable pageable);
PageImpl<FarmLog> findByLikerQueryDslPaging(User liker, Pageable pageable);
}
- 페이징 처리를 위해 쿼리 모두 Pageable 객체를 인자로 받았다.
- PageImpl<T>는 Pageable 정보를 토대로 구성한 page 정보와 함께 실제 쿼리 결과가 담겨있다. hasNext()를 통해 다음 페이지가 존재하는지 등의 정보를 활용할 수 있다.
그리고 Repository가 implement하는 인터페이스에 방금 생성한 customRepository를 추가한다.
@Repository
public interface FarmLogRepository extends JpaRepository<FarmLog,Long>, FarmLogCustomRepository {
}
마지막으로 querydsl method를 실제로 구현할 implement 클래스를 생성한다.
@Repository
@RequiredArgsConstructor
public class FarmLogCustomRepositoryImpl implements FarmLogCustomRepository{
// 빈으로 등록한 jpaQueryFactory
private final JPAQueryFactory jpaQueryFactory;
// 여기에 실제 query method 작성
}
이로써 서비스 컴포넌트에서 FarmLogRepository를 불러오면 FarmLogCustomRepository의 custom query들을 모두 볼 수 있을 것이다.
4. 쿼리 작성
아래 나오는 모든 method들은 FarmLogCustomRepositoryImpl에서 구현한 내용들이다.
1) 모든 일기 불러오기
paging + fetch join을 한꺼번에 하려고 했는데 오류가 떴다.
이와 관련해서 삽질한 내용은 링크 참고 ㅜㅜ 추가로 default_batch_size로 N+1문제를 막으려고 했지만 또다른 문제 때문에 fetch join을 추가로 계속 쓸 수밖에 없었다.
@Override
public PageImpl<FarmLog> findAllFarmLogsQueryDslPaging(Pageable pageable) {
QFarmLog farmLog = QFarmLog.farmLog;
// limit 절만 사용할 query 따로 뺌
List<Long> farmLogIds = jpaQueryFactory
.select(farmLog.farmLogId)
.from(farmLog)
.orderBy(farmLog.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
// fetchJoin
List<FarmLog> farmLogs = jpaQueryFactory.selectFrom(farmLog)
.leftJoin(farmLog.likers).fetchJoin()
.where(farmLog.farmLogId.in(farmLogIds))
.orderBy(farmLog.createdAt.desc())
.fetch();
Long count = jpaQueryFactory.select(farmLog.count()).from(farmLog).fetchOne();
return new PageImpl<>(farmLogs,pageable,(count==null) ? 0 : count);
}
- PageImpl의 생성 인자로 결과 List, Pageable 객체, 전체 쿼리 결과의 수를 넘겨줬다. 해당 정보를 통해 PageImpl은 현재 페이지의 정보(이전 및 다음 페이지의 존재 여부 등)를 제공할 것이다.
- farmLogIds : Pageable 객체에서 받은 offset, pageSize 값을 이용해서 특정 페이지의 페이지 크기(프로젝트에선 10)만큼의 결과 객체들을 뽑아온다.
- farmLogs : 페이징된 결과물인 farmLogId를 실제로 불러온다. 이때 fetch join을 함께 한다.
- count는 select쿼리인데, null 처리를 위해 삼항 연산자를 사용했다.
2) 팔로우하는 사람들의 일기 불러오기
@Override
public PageImpl<FarmLog> findFollowingFarmLogsQueryDslPaging(
final Pageable pageable,
final User follower) {
QFarmLog farmLog = QFarmLog.farmLog;
QFollow follow = QFollow.follow;
// 팔로우하는 사람들의 일기 불러오는 "query"
JPAQuery<Long> followingQuery = jpaQueryFactory.select(farmLog.farmLogId)
.from(farmLog)
.where(farmLog.author.in(
JPAExpressions.select(follow.followed)
.from(follow)
.where(follow.following.eq(follower))
).or(farmLog.author.eq(follower)))
.orderBy(farmLog.createdAt.desc());
long count = followingQuery.stream().count();
// paging
List<FarmLog> followingFarmLogs = jpaQueryFactory
.selectFrom(farmLog)
.leftJoin(farmLog.likers).fetchJoin()
.where(farmLog.farmLogId.in(
followingQuery
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch()))
.orderBy(farmLog.createdAt.desc())
.fetch();
return new PageImpl<>(followingFarmLogs,
pageable,
count);
}
- followingQuery : 팔로우하는 사람들의 일기를 불러오는 queydsl의 쿼리문 자체를 지역변수로 뽑았다. 팔로우 하는 사람들은 JPQL 쿼리 인스턴스를 담은 JPAExpression 내부 쿼리를 이용했다. 이번 프로젝트에서 querydsl의 강력함을 가장 잘 느꼈던 부분이다.
- followingFarmLogs : followingQuery의 페이징을 내부쿼리로 진행한 후 fetchJoin을 시켰다.
3) 특정 유저가 작성한 일기 보기
@Override
public PageImpl<FarmLog> findByAuthorQueryDslPaging(
final User author,
final Pageable pageable) {
QFarmLog farmLog = QFarmLog.farmLog;
List<Long> farmLogIds = jpaQueryFactory
.select(farmLog.farmLogId)
.from(farmLog)
.orderBy(farmLog.createdAt.desc())
.where(farmLog.author.eq(author))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
List<FarmLog> farmLogs = jpaQueryFactory
.selectFrom(farmLog)
.where(farmLog.farmLogId.in(
farmLogIds))
.leftJoin(farmLog.likers).fetchJoin()
.orderBy(farmLog.createdAt.desc())
.fetch();
Long count = jpaQueryFactory
.select(farmLog.count())
.from(farmLog)
.where(farmLog.author.eq(author))
.fetchOne();
return new PageImpl<>(farmLogs, pageable, count==null ? 0 : count);
}
- where절에 특정 유저의 조건이 있는 것을 제외하면 1번과 같은 쿼리이다.
4) 특정 유저가 "좋아요"한 일기 보기
@Override
public PageImpl<FarmLog> findByLikerQueryDslPaging(final User liker,
final Pageable pageable) {
QFarmLog farmLog = QFarmLog.farmLog;
QGood good = QGood.good;
// 해당 유저가 좋아요한 일기 Id 페이징
List<Long> farmLogIds = jpaQueryFactory
.select(farmLog.farmLogId).from(farmLog)
.where(farmLog.in(JPAExpressions
.select(good.farmLog).from(good)
.where(good.liker.eq(liker))))
.orderBy(farmLog.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
// fetchJoin
List<FarmLog> farmLogs = jpaQueryFactory
.selectFrom(farmLog)
.leftJoin(farmLog.likers).fetchJoin()
.where(farmLog.farmLogId.in(
farmLogIds))
.orderBy(farmLog.createdAt.desc())
.fetch();
Long count = jpaQueryFactory
.select(farmLog.count())
.from(farmLog)
.where(farmLog.in(
JPAExpressions
.select(good.farmLog).from(good)
.where(good.liker.eq(liker))))
.fetchOne();
return new PageImpl<>(farmLogs, pageable, count == null ? 0 : count);
}
5. 마무리
페이징 + fetch join이 똑바로 안된다는게 가장 곤란했던 부분이었다. 과거 hibernate의 일대다 관계에서 fetch join시에 카테시안 곱 문제가 있었다가 현재 와서 사라진 것처럼, 이것도 언젠간 개선이 될까?
REFERENCE:
https://wildeveloperetrain.tistory.com/135
자바 언어로 JPQL을 작성할 수 있게 해주는 오픈소스 프레임워크 querydsl을 프로젝트에 적용시켜 보도록 하겠다.
Sorting 관련 내용은 따로 복잡하게 하지 않았고, 기본적인 내용은 다른 포스트에서 설명한 만큼 이번에는 페이징까지 한꺼번에 처리하도록 하겠다.
2023.03.21 - [JAVA/Spring] - [QueryDSL] QueryDSL이란? + 간단한 실습 << querydsl에 관한 내용과 프로젝트 설정은 이곳을 참고하자.
1. 적용 준비 (build.gradle, configuration)
- gradle 7.6
- JAVA 17
- querydsl 5.0.0
- spring boot 3.0.2
전술한 블로그 링크 문서와 겹치는 설정 내용이다. build.gradle에 dependency를 추가한다.
dependencies {
// QueryDSL 관련 dependencies
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
// build.gradle 파일의 가장 밑에 아래 추가
def querydslDir = "$buildDir/generated/querydsl"
// build할 때 소스코드로 인식할 범위에 querydslDir추가
sourceSets {
main.java.srcDirs += [ querydslDir ]
}
// compile 단계에서 Q클래스 생성 디렉토리 설정
tasks.withType(JavaCompile) {
options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}
clean.doLast {
file(querydslDir).deleteDir()
}
프로젝트를 빌드하고, querydsl에서 쿼리를 작성하는데 쓰이는 클래스인 Q클래스가 생성되었음을 확인한 후 queryFactory 빈을 configuration한다. querydsl의 쿼리가 필요할 때마다 entityManager 객체를 불러오는 방법도 있지만, 번거롭기 때문에 쿼리팩토리 자체를 빈으로 등록했다.
@Configuration
public class QueryDslConfiguration {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
연관 관계 설명때 언급했지만, 현재 프로젝트에서 엔티티간 연관관계는 총 3가지이다.
- user - farm_log : 일대다. 한 명의 유저(user)가 여러개의 농장일기(farm_log)를 가지고 있다.
- user - user : 다대다. 한 명의 유저(user)는 여러 명의 유저(user)을 팔로잉 할 수 있으며, 또한 여러 명의 유저(user)에게 팔로잉 당할 수 있다. 해당 관계는 follow 연관 테이블에 매핑되어 있다.
- user - farm_log : 다대다. 한 명의 유저(user)는 여러 개의 농장일기(farm_log)에 좋아요를 누를 수 있으며, 또한 한 개의 농장일기(farm_log)는 여러 명의 유저에게 좋아요를 받을 수 있다.
ERD)

동적 쿼리 + 페이징 처리로 작성할 쿼리는 4가지이다.
- 홈 화면에서 보여지는 "모든 농장일기 보기" -> 모든 농장일기를 불러오되, 10개씩 페이징 처리를 할 것이다.
- 홈 화면에서 보여지는 "팔로우한 사람들의 농장일기 보기" -> 내가 작성한 일기와 팔로우한 사람들의 일기만 보이게 할 것이다.
- 특정 유저 페이지에서 보여지는 "해당 유저가 작성한 농장일기 보기" -> 유저 상세보기 페이지의 유저가 작성한 일기만 보이게 할 것이다.
- 특정 유저 페이지에서 보여지는 "해당 유저가 좋아요한 농장일기 보기" -> 유저 상세보기 페이지의 유저가 좋아요한 일기만 보이게 할 것이다.
2. Custom PageRequst
스프링 프레임워크에선 paging을 위해 Pageable 인터페이스와 PageRequest 클래스를 지원한다. (PageRequest가 Pageable을 implement하는 구조)
페이징에 필요한 페이지 번호(page), 페이지 사이즈(size), 정렬 정보(sort) 객체를 받아 PageRequest를 구성하여 offset, limit을 가져와 실제 쿼리에 이용하는 구조이다.
페이징에 필요한 페이지 번호는 query parameter로서 받는다. page, size, sort 각각을 아래와 같은 url 링크로 받는다.
/board?page=0&size=20&sort=created_at,desc
spring mvc의 handler method parameter로 PageRequest 혹은 Pageable 객체를 두면, 아래와 같은 방법을 통해 query parameter로 들어온 값을 Pageable 객체로 사용할 수 있다.
import org.springframework.data.domain.PageRequest;
@GetMapping("/pageRequest")
public String pagingExample(PageRequest pageRequest) {
Pageable pageable = pageRequets.of();
// of() static method 통해
// pageable의 offset, pageSize 등 접근 가능
}
그러나 위와 같이 dafault PageRequest 객체를 이용하면 몇 가지 단점이 있다.
- 페이지 시작점이 0번부터라는 점 (1부터 시작하는 것이 직관적으로 좋다)
- 페이지의 사이즈가 임의조절 될 수 있고, 그 최대값이 정해지지 않다는 점
이외에도 기타 예외처리를 하기 어려워 직접 PageRequest 객체를 만들어 사용하는 것이 좋다.
프로젝트에선 페이지 사이즈를 10으로 고정했다.
public interface UtilConst {
int DEFAULT_PAGE_SIZE = 10;
}
그리고 1부터 시작하는 페이지 번호를 기준으로, sort나 pageSize 관련 정보는 받지 않고 페이지 번호만을 받았다. 페이징을 조금 더 심오하게 진행하면 각 필드의 정렬 방향(desc, asc)을 결정하는 항목도 추가할 수 있겠지만, 지금은 단순히 페이지만 나누는 기능을 구현할 목적이므로
public class PageRequest {
private int page = 1;
public void setPage(String page) {
try {
this.page = Math.min(1,Integer.parseInt(page));
} catch (NumberFormatException e) {
this.page = 1;
}
}
public int getPage() {
return page;
}
public org.springframework.data.domain.PageRequest of() {
return org.springframework.data.domain.PageRequest.of(
this.page - 1, UtilConst.DEFAULT_PAGE_SIZE);
}
}
- NumberFormatException e : 쿼리 파라미터로 들어온 page 값이 int가 아닌 예외를 처리해주었다.
- of() : custom PageRequest에서 전처리를 한 PageRequest를 바탕으로 스프링에서 쓰는 PageRequest를 return한다. 페이지 번호의 시작이 0부터이므로 custom PageRequest의 page 번호에 1을 빼줬다.
이제 handler method의 인자에 위 POJO 클래스를 집어넣으면, methodArgumentResolver가 정의한 setPage()를 호출해 실제 스프링 프레임워크에서 지원하는 Pageabl을 사용할 수 있다.
@Controller
@RequiredArgsConstructor
public class MyController {
@GetMapping("/home")
public String getHome(Model model,
final PageRequest pageRequest) {
Pageable pageable = pageRequest.of();
return "index.html";
}
}
3. Custom Repository
queryDsl의 쿼리를 이용할 custom query들을 정의했다. 앞서 언급한 4개의 쿼리들이다.
public interface FarmLogCustomRepository {
PageImpl<FarmLog> findAllFarmLogsQueryDslPaging(Pageable pageable);
PageImpl<FarmLog> findFollowingFarmLogsQueryDslPaging(Pageable pageable, User following);
PageImpl<FarmLog> findByAuthorQueryDslPaging(User author, Pageable pageable);
PageImpl<FarmLog> findByLikerQueryDslPaging(User liker, Pageable pageable);
}
- 페이징 처리를 위해 쿼리 모두 Pageable 객체를 인자로 받았다.
- PageImpl<T>는 Pageable 정보를 토대로 구성한 page 정보와 함께 실제 쿼리 결과가 담겨있다. hasNext()를 통해 다음 페이지가 존재하는지 등의 정보를 활용할 수 있다.
그리고 Repository가 implement하는 인터페이스에 방금 생성한 customRepository를 추가한다.
@Repository
public interface FarmLogRepository extends JpaRepository<FarmLog,Long>, FarmLogCustomRepository {
}
마지막으로 querydsl method를 실제로 구현할 implement 클래스를 생성한다.
@Repository
@RequiredArgsConstructor
public class FarmLogCustomRepositoryImpl implements FarmLogCustomRepository{
// 빈으로 등록한 jpaQueryFactory
private final JPAQueryFactory jpaQueryFactory;
// 여기에 실제 query method 작성
}
이로써 서비스 컴포넌트에서 FarmLogRepository를 불러오면 FarmLogCustomRepository의 custom query들을 모두 볼 수 있을 것이다.
4. 쿼리 작성
아래 나오는 모든 method들은 FarmLogCustomRepositoryImpl에서 구현한 내용들이다.
1) 모든 일기 불러오기
paging + fetch join을 한꺼번에 하려고 했는데 오류가 떴다.
이와 관련해서 삽질한 내용은 링크 참고 ㅜㅜ 추가로 default_batch_size로 N+1문제를 막으려고 했지만 또다른 문제 때문에 fetch join을 추가로 계속 쓸 수밖에 없었다.
@Override
public PageImpl<FarmLog> findAllFarmLogsQueryDslPaging(Pageable pageable) {
QFarmLog farmLog = QFarmLog.farmLog;
// limit 절만 사용할 query 따로 뺌
List<Long> farmLogIds = jpaQueryFactory
.select(farmLog.farmLogId)
.from(farmLog)
.orderBy(farmLog.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
// fetchJoin
List<FarmLog> farmLogs = jpaQueryFactory.selectFrom(farmLog)
.leftJoin(farmLog.likers).fetchJoin()
.where(farmLog.farmLogId.in(farmLogIds))
.orderBy(farmLog.createdAt.desc())
.fetch();
Long count = jpaQueryFactory.select(farmLog.count()).from(farmLog).fetchOne();
return new PageImpl<>(farmLogs,pageable,(count==null) ? 0 : count);
}
- PageImpl의 생성 인자로 결과 List, Pageable 객체, 전체 쿼리 결과의 수를 넘겨줬다. 해당 정보를 통해 PageImpl은 현재 페이지의 정보(이전 및 다음 페이지의 존재 여부 등)를 제공할 것이다.
- farmLogIds : Pageable 객체에서 받은 offset, pageSize 값을 이용해서 특정 페이지의 페이지 크기(프로젝트에선 10)만큼의 결과 객체들을 뽑아온다.
- farmLogs : 페이징된 결과물인 farmLogId를 실제로 불러온다. 이때 fetch join을 함께 한다.
- count는 select쿼리인데, null 처리를 위해 삼항 연산자를 사용했다.
2) 팔로우하는 사람들의 일기 불러오기
@Override
public PageImpl<FarmLog> findFollowingFarmLogsQueryDslPaging(
final Pageable pageable,
final User follower) {
QFarmLog farmLog = QFarmLog.farmLog;
QFollow follow = QFollow.follow;
// 팔로우하는 사람들의 일기 불러오는 "query"
JPAQuery<Long> followingQuery = jpaQueryFactory.select(farmLog.farmLogId)
.from(farmLog)
.where(farmLog.author.in(
JPAExpressions.select(follow.followed)
.from(follow)
.where(follow.following.eq(follower))
).or(farmLog.author.eq(follower)))
.orderBy(farmLog.createdAt.desc());
long count = followingQuery.stream().count();
// paging
List<FarmLog> followingFarmLogs = jpaQueryFactory
.selectFrom(farmLog)
.leftJoin(farmLog.likers).fetchJoin()
.where(farmLog.farmLogId.in(
followingQuery
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch()))
.orderBy(farmLog.createdAt.desc())
.fetch();
return new PageImpl<>(followingFarmLogs,
pageable,
count);
}
- followingQuery : 팔로우하는 사람들의 일기를 불러오는 queydsl의 쿼리문 자체를 지역변수로 뽑았다. 팔로우 하는 사람들은 JPQL 쿼리 인스턴스를 담은 JPAExpression 내부 쿼리를 이용했다. 이번 프로젝트에서 querydsl의 강력함을 가장 잘 느꼈던 부분이다.
- followingFarmLogs : followingQuery의 페이징을 내부쿼리로 진행한 후 fetchJoin을 시켰다.
3) 특정 유저가 작성한 일기 보기
@Override
public PageImpl<FarmLog> findByAuthorQueryDslPaging(
final User author,
final Pageable pageable) {
QFarmLog farmLog = QFarmLog.farmLog;
List<Long> farmLogIds = jpaQueryFactory
.select(farmLog.farmLogId)
.from(farmLog)
.orderBy(farmLog.createdAt.desc())
.where(farmLog.author.eq(author))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
List<FarmLog> farmLogs = jpaQueryFactory
.selectFrom(farmLog)
.where(farmLog.farmLogId.in(
farmLogIds))
.leftJoin(farmLog.likers).fetchJoin()
.orderBy(farmLog.createdAt.desc())
.fetch();
Long count = jpaQueryFactory
.select(farmLog.count())
.from(farmLog)
.where(farmLog.author.eq(author))
.fetchOne();
return new PageImpl<>(farmLogs, pageable, count==null ? 0 : count);
}
- where절에 특정 유저의 조건이 있는 것을 제외하면 1번과 같은 쿼리이다.
4) 특정 유저가 "좋아요"한 일기 보기
@Override
public PageImpl<FarmLog> findByLikerQueryDslPaging(final User liker,
final Pageable pageable) {
QFarmLog farmLog = QFarmLog.farmLog;
QGood good = QGood.good;
// 해당 유저가 좋아요한 일기 Id 페이징
List<Long> farmLogIds = jpaQueryFactory
.select(farmLog.farmLogId).from(farmLog)
.where(farmLog.in(JPAExpressions
.select(good.farmLog).from(good)
.where(good.liker.eq(liker))))
.orderBy(farmLog.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
// fetchJoin
List<FarmLog> farmLogs = jpaQueryFactory
.selectFrom(farmLog)
.leftJoin(farmLog.likers).fetchJoin()
.where(farmLog.farmLogId.in(
farmLogIds))
.orderBy(farmLog.createdAt.desc())
.fetch();
Long count = jpaQueryFactory
.select(farmLog.count())
.from(farmLog)
.where(farmLog.in(
JPAExpressions
.select(good.farmLog).from(good)
.where(good.liker.eq(liker))))
.fetchOne();
return new PageImpl<>(farmLogs, pageable, count == null ? 0 : count);
}
5. 마무리
페이징 + fetch join이 똑바로 안된다는게 가장 곤란했던 부분이었다. 과거 hibernate의 일대다 관계에서 fetch join시에 카테시안 곱 문제가 있었다가 현재 와서 사라진 것처럼, 이것도 언젠간 개선이 될까?
REFERENCE:
https://wildeveloperetrain.tistory.com/135