@Query("select p from Post p join fetch p.user u "
+ "where u in "
+ "(select t from Follow f inner join f.target t on f.source = :user) "
+ "or u = "user "
+ "order by p .createdAt desc")
List<Post> findAllAssociatedPostsByUser(@Param("user") User user, Pageable pageable);
Spring Data JPA가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능을 사용하더라도 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 JPQL을 작성하게 됩니다. 간단한 로직은 큰 문제가 없으나 복잡한 로직의 경우 개행이 포함된 쿼리 문자열이 상당히 길어집니다. JPQL 문자열에 오타 혹은 문법적인 오류가 존재하는 경우, 정적 쿼리라면 어플리케이션 로딩 시점에 이를 발견할 수 있으나 그 외는 런타임 시점에서 에러가 발생합니다.
이러한 문제를 어느정도 해소하는데 기여하는 프레임워크가 바로 QueryDSL 입니다.
QueryDSL은 정적 타입을 이용해서 SQL 등의 쿼리를 생성해주는 프레임워크입니다.
// build.gradle
dependencies{
// queryDSL 설정
implementation "com.querydsl:querydsl-jpa"
implementation "com.querydsl:querdsl-core"
implementation "com.querydsl:querydsl-collections"
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa" // queryDSL JPAAnnotationProcessor 사용 지정
annotationProcessor "jakarta.annotation:jakarta.annotation-api" // java.lang.NoClassDefFoundError (javax.annotation.Generated) 대응 코드
annotationProcessor "jakarta.persistence:jakarta.persistence-api" // java.lang.NoClassDefFoundError (javax.annotation.Entity) 대응 코드
}
// QueryDSL 설정부
def generated = 'src/main/generated'
// queryDSL Qclass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(generated))
}
// java source set에 querydsl QClass 위치 추가
sourceSets {
main.java.srcDirs += [ generated ]
}
// gradle clean 시에 QClass 디렉토리 삭제
clean {
delete file(generated)
}
위 설정이후 gradle을 build하게 되면 아래 그림과 같이 Q 클래스가 추가됩니다.
@Getter
@ToString(callSuper = true)
@Table(indexes = {
@Index(columnList = "title"),
@Index(columnList = "hashtag"),
@Index(columnList = "createdAt"),
@Index(columnList = "createdBy")
})
@Entity
public class Article extends AuditingFields {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Setter @ManyToOne(optional = false) @JoinColumn(name = "userId") private UserAccount userAccount; // 유저 정보 (ID)
@Setter @Column(nullable = false) private String title; // 제목
@Setter @Column(nullable = false, length = 10000) private String content; // 본문
@Setter private String hashtag; // 해시태그
@ToString.Exclude
@OrderBy("createdAt DESC")
@OneToMany(mappedBy = "article", cascade = CascadeType.ALL)
private final Set<ArticleComment> articleComments = new LinkedHashSet<>();
protected Article() {}
private Article(UserAccount userAccount, String title, String content, String hashtag) {
this.userAccount = userAccount;
this.title = title;
this.content = content;
this.hashtag = hashtag;
}
public static Article of(UserAccount userAccount, String title, String content, String hashtag) {
return new Article(userAccount, title, content, hashtag);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Article that)) return false;
return id != null && id.equals(that.getId());
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
hi라는 내용을 포함하며 댓글이 1개 이상인 Post를 ID 내림차순으로 조회하는 로직이 존재한다고 가정.
정적 쿼리가 아닌 EntityManager를 통해 JPQL을 작성하는 경우
PostRepositoryTest.java
@DisplayName("hi 내용을 포함하며 댓글이 1개 이상인 Post를 조회한다.")
@Test
void jpa_findPostByMyCriteria_Three(){
EntityManager entityManager = testEntityManager.getEntityManager();
List<Post> posts = entityManager.createQuery("select p from Post p where p.content like '%hi%' and p.comments.size > 0 order by p.id desc", Post.class)
.getResultList();
assertThat(posts).hasSize(3);
}
PostRepositoryTest.java
@DisplayName("hi 내용을 포함하며 댓글이 1개 이상인 Post를 ID 내림차순으로 조회한다.") @Test void queryDsl_findPostByMyCriteria_Three(){ EntityManager entityManager = testEntityManager.getEntityManager();
JPAQuery<Post> query = new JPAQuery<>(entityManager);
QPost qPost = new QPost("p");
QComment qComment = new QComment("c");
List<Post> posts = query.distinct()
.from(qPost)
.leftJoin(qPost.comments, qComment).fetchJoin()
.fetch();
assertThat(posts).hasSize(3);
}
- fetchJoin() 등 직관적인 체이닝 메서드를 통해 간단하게 Fetch Join을 적용할 수 있습니다.
### Repository에서 QueryDSL 사용하기
> PostRepository.java
public interface PostRepository extends JpaRepository<Post, Long> {
@Query("select p from Post p join fetch p.comments")
List<Post> findAllInnerFetchJoin();
@Query("select distinct p from Post p join fetch p.comments")
List<Post> findAllInnerFetchJoinWithDistinct();
}
- 현재 PostRepository가 사용중인 정적 쿼리(JPQL)들을 QueryDSL로 교체 해봅시다.(Spring Data JPA는 JpaRepository를 상속한 Repository 클래스 (예, PostRepository)에서 Custom Repository 기능을 사용할 수 있도록 하는 기능을 제공합니다.)
> PostCustomRepository.java
public interface PostCustomRepository{
List findAllInnerFetchJoin();
List<Post> findAllInnerFetchJoinWithDistinct();
}
> PostCustomRepositoryImpl.java
public class PostCustomRepositoryImpl extends QuerydslRepositorySupport implements PostCustomRepository{
@Override
public List<Post> findAllInnerFetchJoin(){
return from(post)
.innerJoin(post.comments)
.fetchJoini()
.fetch();
}
@Override
public List<Post> findAllInnerFetchJoinWithDistinct(){
return from(post)
.innerJoin(post.comments)
.fetchJoin()
.fetch();
}
}
- 커스텀 인터페이스를 구현하는 클래스에 QueryDSL 쿼리를 작성합니다.
이 때, 해당 구현 클래스 이름은 반드시 Impl로 끝나야 합니다.
Impl로 끝내고싶지 않으면 설정을 별도로 해야 합니다.
> PostRepository.java
public interface PostRepository extends JpaRepository<Post, Long>, PostCustomRepository{
}
- JpaRepository를 상속하는 PostRepository가 PostCustomRepository 인터페이스를 상속하도록 합니다.
- PostCustomRepositoryImpl에 작성된 QueryDSL 코드를 PostRepository가 자동으로 사용할 수 있게 됩니다.
참조 url
https://tecoble.techcourse.co.kr/post/2021-08-08-basic-querydsl/