[ํ๋ก์ ํธ] JPQL → QueryDSL ๋ก ๋ณ๊ฒฝํ๊ธฐBack-end/Project2024. 9. 12. 15:24
Table of Contents
๐ก QueryDSL ์ด๋?
ํ์ด๋ฒ๋ค์ดํธ ์ฟผ๋ฆฌ ์ธ์ด(HQL: Hibernate Query Language)์ ์ฟผ๋ฆฌ๋ฅผ ํ์ ์ ์์ ํ๊ฒ ์์ฑ ๋ฐ ๊ด๋ฆฌํด์ฃผ๋ ํ๋ ์์ํฌ
QueryDSL์ด ๋ฑ์ฅํ๊ธฐ ์ด์ ์๋ Mybatis, JPQL, Criteria ๋ฑ ๋ฌธ์์ด ํํ๋ก ์ฟผ๋ฆฌ๋ฌธ์ ์์ฑํ์ฌ ์ปดํ์ผ ์์ ์ค๋ฅ๋ฅผ ๋ฐ๊ฒฌํ๋ ๊ฒ์ด ๋ถ๊ฐ๋ฅํ์
QueryDSL์ JPA ๋ฟ๋ง ์๋๋ผ SQL, MongoDB, Lucenece ๋ฑ๋ค์ํ ์ธ์ด์ ๋ํด์ ์๋น์ค๋ฅผ ์ ๊ณตํจ
QueryDSL์ ์ฌ์ฉํ์ฌ ๊ธฐ์กด์ @Query๋ฅผ ๋์ฒดํ๋ ค๋ฉด, JPAQueryFactory๋ฅผ ์ฌ์ฉํด ๋์ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ ์ ์์
1. Qํด๋์ค ์์ฑ
- ํด๋น ์ํฐํฐ์ ํ๋๋ค์ ๊ฐ์ฒด๋ก ๋ค๋ฃจ๋ ๋๊ตฌ
- QueryDSL์ด ์ปดํ์ผ ์ ์๋์ผ๋ก ์์ฑ (Post → QPost)
public class QPost extends EntityPathBase<Post> { public static final QPost post = new QPost("post"); public final StringPath title = createString("title"); public final DateTimePath<LocalDateTime> createdAt = createDateTime("createdAt", LocalDateTime.class); // other fields }
2. ์ฟผ๋ฆฌ ์์ฑ ๋ฐ ๋น๋ฉ
- QueryDSL์ด JPAQuery ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ์ฌ ์ฟผ๋ฆฌ๋ฅผ ์์ฑ
- Qํด๋์ค์ ํ๋๋ค์ ์ด์ฉํ์ฌ ๋์ ์ฟผ๋ฆฌ๋ฅผ ์์ฑ
QPost post = QPost.post; List<Post> posts = new JPAQuery<>(entityManager) .select(post) // Select post .from(post) // From Post table .where(post.title.eq("test")) // Where title = 'test' .fetch(); // ์ฟผ๋ฆฌ ์คํ
๐ก QueryDSL vs JPQL
QueryDSL | JPQL | |
์ฟผ๋ฆฌ ์์ฑ ๋ฐฉ์ | ๋ฉ์๋ ์ฒด์ธ ๊ธฐ๋ฐ | ๋ฌธ์์ด ๊ธฐ๋ฐ |
์ค๋ฅ ๊ฒ์ถ | ์ปดํ์ผ ํ์์ ์ค๋ฅ ๊ฒ์ถ ๊ฐ๋ฅ | ๋ฐํ์ ์ ์ค๋ฅ ๋ฐ์ ๊ฐ๋ฅ |
๋์ ์ฟผ๋ฆฌ ์์ฑ | ๋์ ์ฟผ๋ฆฌ ์์ฑ์ด ๋งค์ฐ ์ ์ฐํจ | ๋ถํธํจ |
๊ฐ๋ ์ฑ ๋ฐ ์ ์ง๋ณด์ | ๊ฐ์ฒด์งํฅ์ ์ฟผ๋ฆฌ ์์ฑ ๋ฐฉ์์ผ๋ก ๊ฐ๋ ์ฑ ๋์ | ๋ณต์กํ ์ฟผ๋ฆฌ์์๋ ๊ฐ๋ ์ฑ์ด ๋จ์ด์ง ์ ์์ |
๐พ QueryDSL ์ค์ ์ฝ๋
build.gradle
dependencies {
// queryDSL ์ค์
implementation "com.querydsl:querydsl-jpa:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
implementation "com.querydsl:querydsl-core"
implementation "com.querydsl:querydsl-collections"
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" // 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 ์ค์ ๋ถ
// ์์ผ๋ฉด QClass๋ค์ด ๋น๋ ๋๋ ํ ๋ฆฌ์ ๋ค์ด๊ฐ -> gradle์ด ๋น๋ํ๋ ค๊ณ ์ค์บํ ์์ญ๊ณผ intellij๊ฐ ์ค์บํ๊ณ ์ ํ๋ ๋น๋ํด๋์ค ํ์ผ ์์ญ์ ํ๋ฒ ๋ ์ค์บํ๋ฉด์ ์ถฉ๋๋๋ฉด์ ์ค๋ณต๋ฌธ์ ๋ฐ์
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)
}
.gitignore ์ ์ถ๊ฐ
### Querydsl
/src/main/generated
๐ ์์ 1
public class HashtagService {
private final HashtagRepository hashtagRepository;
public void deleteUnusedHashtags(Set<Long> hashtagIds) {
List<Hashtag> unusedHashtags = hashtagRepository.findUnusedHashtagsByIds(hashtagIds);
for (Hashtag unusedHashtag : unusedHashtags) {
hashtagRepository.delete(unusedHashtag);
}
}
}
โ๏ธ ๊ธฐ์กด ์ฝ๋:: JPQL
public interface HashtagRepository extends
JpaRepository<Hashtag, Long>
{
// ํด์ํ๊ทธ ์ค์์ PostHashtag์ ์ํ์ง ์๋ ํด์ํ๊ทธ๋ค ์ฐพ๊ธฐ
@Query("SELECT h FROM Hashtag h WHERE h.postHashtags IS EMPTY AND h.id IN :hashtagIds")
List<Hashtag> findUnusedHashtagsByIds(@Param("hashtagIds") Set<Long> hashtagIds);
}
โ๏ธ ๋ณ๊ฒฝ๋ ์ฝ๋:: QueryDSL
public interface HashtagRepository extends
JpaRepository<Hashtag, Long>,
HashtagRepositoryCustom
{
}
public interface HashtagRepositoryCustom {
List<Hashtag> findUnusedHashtagsByIds(@Param("hashtagIds") Set<Long> hashtagIds);
}
public class HashtagRepositoryCustomImpl extends QuerydslRepositorySupport implements HashtagRepositoryCustom {
public HashtagRepositoryCustomImpl() {
super(Hashtag.class);
}
@Override
public List<Hashtag> findUnusedHashtagsByIds(Set<Long> hashtagIds) {
QHashtag hashtag = QHashtag.hashtag;
return from(hashtag)
.select(hashtag)
.where(hashtag.postHashtags.isEmpty()
.and(hashtag.id.in(hashtagIds)))
.fetch();
}
}
QHashtag.hashtag: QueryDSL์์ ์์ฑ๋ Q ํด๋์ค
hashtag.postHashtags.isEmpty(): postHashtags๊ฐ ๋น์ด ์๋์ง ํ์ธํ๋ ์กฐ๊ฑด
hashtag.id.in(hashtagIds): id๊ฐ hashtagIds์ ํฌํจ๋๋์ง ํ์ธํ๋ ์กฐ๊ฑด
fetch(): ์ฟผ๋ฆฌ ์คํ ๊ฒฐ๊ณผ๋ฅผ ๋ฆฌ์คํธ๋ก ๊ฐ์ ธ์จ๋ค
∴ postHashtags๊ฐ ๋น์ด ์๊ณ , id๊ฐ ํน์ ์งํฉ์ ํฌํจ๋ Hashtag ์ํฐํฐ๋ค์ ์กฐํ
๐
public class HashtagRepositoryCustomImpl extends QuerydslRepositorySupport implements HashtagRepositoryCustom
HashtagRepositoryCustomImpl
- Impl ๋ถ์ฌ์ฃผ๋ฉด querydsl์ด ์ธ์ ๊ฐ๋ฅ (๊ท์ฝ)
- JPA์ ๊ธฐ๋ณธ ๊ธฐ๋ฅ ์ธ์ ์ถ๊ฐ์ ์ธ ์ฟผ๋ฆฌ ๋ก์ง์ ์ ์ํ๊ธฐ ์ํด ๊ตฌํ์ฒด ์์ฑํจ
extends QuerydslRepositorySupport
- Spring Data JPA ์์ ์ ๊ณตํ๋ ํด๋์ค
- QuerydslRepositorySupport ๋ฅผ ์์ํ๋ฉด JPAQueryFactory ๋ฅผ ์๋์ผ๋ก ์ฌ์ฉํ ์ ์์ด, ๋ณ๋๋ก ์์ฑํ์ง ์๊ณ ๋
QueryDSL ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ ์ ์๋ค
implements HashtagRepositoryCustom
- HashtagRepositoryCustom ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํ
๐
public HashtagRepositoryCustomImpl() { super(Hashtag.class); }
super : ๋ถ๋ชจ ํด๋์ค์ ์์ฑ์๋ฅผ ํธ์ถํ๋ ์ญํ
QuerydslRepositorySupport ํด๋์ค๋ ์์๋ฐ๋ ํด๋์ค์์ ๊ด๋ฆฌํ ์ํฐํฐ ํด๋์ค๋ฅผ ํ์๋ก ํ๋ค
super(Hashtag.class); : QuerydslRepositorySupport ๊ฐ Hashtag ์ํฐํฐ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ฟผ๋ฆฌ๋ฅผ ์ํํ ์ ์๋๋ก ์ํฐํฐ
ํด๋์ค์ ํ์ ์ ๋ถ๋ชจ ํด๋์ค์๊ฒ ์ ๋ฌ
โ๏ธ ์ฟผ๋ฆฌ ์คํ ๊ฒฐ๊ณผ
Hibernate:
select
h1_0.id,
h1_0.created_at,
h1_0.created_by,
h1_0.hashtag_name,
h1_0.modified_at,
h1_0.modified_by
from
hashtag h1_0
where
not exists(select
1
from
post_hashtag ph1_0
where
h1_0.id=ph1_0.hashtag_id)
and h1_0.id in (?, ?)
2024-09-12T14:47:24.937+09:00 TRACE 68585 --- [nio-8080-exec-2] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [19]
2024-09-12T14:47:24.938+09:00 TRACE 68585 --- [nio-8080-exec-2] org.hibernate.orm.jdbc.bind : binding parameter (2:BIGINT) <- [20]
๐ ์์ 2 - Join
Post post = notificationRepository.findPostByNotificationId(id)
.orElseThrow(() -> new EntityNotFoundException("์๋ฆผ์ ํด๋นํ๋ ํฌ์คํธ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค - notiId: " + id));
โ๏ธ QueryDSL
public interface NotificationRepository extends
JpaRepository<Notification, Long>,
NotificationRepositoryCustom
{
}
public interface NotificationRepositoryCustom {
Optional<Post> findPostByNotificationId(Long id);
}
public class NotificationRepositoryCustomImpl extends QuerydslRepositorySupport implements NotificationRepositoryCustom {
public NotificationRepositoryCustomImpl() {
super(Notification.class);
}
@Override
public Optional<Post> findPostByNotificationId(Long id) {
QNotification notification = QNotification.notification;
QPost post = QPost.post;
QLike like = QLike.like;
QPostComment postComment = QPostComment.postComment;
Notification fetchedNotification = from(notification)
.select(notification)
.where(notification.id.eq(id))
.fetchOne();
// NotificationType์ ๋ฐ๋ผ ๋์ ์กฐ์ธ
switch (fetchedNotification.getNotificationType()) {
case NEW_LIKE_ON_POST:
return Optional.ofNullable(from(like)
.select(post)
.join(like.post, post)
.where(like.id.eq(fetchedNotification.getTargetId()))
.fetchOne());
case NEW_COMMENT_ON_POST:
return Optional.ofNullable(from(postComment)
.select(post)
.join(postComment.post, post)
.where(postComment.id.eq(fetchedNotification.getTargetId()))
.fetchOne());
}
return Optional.empty();
}
}
โ๏ธ ์ฟผ๋ฆฌ ์คํ ๊ฒฐ๊ณผ
Hibernate:
select
p1_0.id,
p1_0.content,
p1_0.created_at,
p1_0.created_by,
p1_0.modified_at,
p1_0.modified_by,
p1_0.title,
p1_0.user_id
from
post_comment pc1_0
join
post p1_0
on p1_0.id=pc1_0.post_id
where
pc1_0.id=?
fetch() vs fetchOne()
- ๋ ๋ค QueryDSL์์ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ ธ์ฌ ๋ ์ฌ์ฉํ๋ ๋ฉ์๋๋ค
- ๋ฐํํ๋ ๋ฐฉ์์์ ์ฐจ์ด๊ฐ ์์
fetch()
- ๋ค์ ๊ฒฐ๊ณผ ๊ฐ์ ธ์ด
- ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ก ๋์จ ๋ ์ฝ๋๋ค → List ํํ๋ก ๋ฐํ (์ฌ๋ฌ ํ์ ์กฐํํ ๋ ์ฌ์ฉ)
fetchOne()
- ํ๋์ ๊ฐ์ฒด๋ ๊ฐ์ด ๋ฐํ๋จ
- ๊ฒฐ๊ณผ๊ฐ ์์ผ๋ฉด Null ๋ฐํ
- ๊ฒฐ๊ณผ๊ฐ ๋ ๊ฐ ์ด์์ด๋ฉด ์์ธ ๋ฐ์