QueryDSL

배세훈·2022년 8월 11일
0

JPA

목록 보기
2/3

QueryDSL

@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 등의 쿼리를 생성해주는 프레임워크입니다.

  • QueryDSL 장점
  1. 문자가 아닌 코드로 쿼리를 작성함으로써 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
  2. 자동 완성 등 IDE의 도움을 받을 수 있다.
  3. 동적인 쿼리 작성이 편리하다.
  4. 쿼리 작성 시 제약 조건 등을 메서드 추출을 통해 재사용할 수 있다.
  • QueryDSL 단점
  1. 번거로운 Gradle 설정 및 사용법 등을 익혀야 한다.

QueryDSL 설정

  • QueryDSL 설정 방법은 Gradle 및 IntelliJ 버전에 따라 상이함.
  • Springboot 2.7.0, java 17 기준, gradle 7.4.1
// 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 클래스가 추가됩니다.

QueryDSL 간단 예제

Article Entity

@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);
    }

}

QueryDSL 사용 Repository

  • 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);
}
  • 정적 쿼리가 아닌 관계로 문법 오류가 발생하면 어플리케이션 로딩시점에서 감지하지 못하고, 런타임 에러가 발생합니다.
  • Post.class 제네릭 타입을 메서드 파라미터로 제공하지 않으면 ,로 구분되어 타입의 리스트가 반환됩니다.

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/

profile
성장형 인간

0개의 댓글