Spring
JPQL 및 QueryDSL 활용
sehunbang
2024. 3. 6. 14:56
1. JPQL
(Java Persistence Query Language)
Table 이 아닌 Entity(객체) 기준으로 작성하는 쿼리를 JPQL 이라고 하며
이를 사용할 수 있도록 EntityManger 또는 @Query 구현체를 통해 JPQL 쿼리를 사용할 수 있다.
1-1. EntityMananger.createQuery()
- 쿼리 문자열과 Entity 를 직접 넣어서 쿼리를 작성한다.
- setParameter 와 같이 key, value 문자열을 통해서 쿼리 파라미터를 매핑할 수 있다.
안 좋은 예시
@Test
public void testEmCreateQuery() {
String qlString = "select u from User u " +
"where u.username = :username";
Member findUser = em.createQuery(qlString, User.class)
.setParameter("username", "sehun")
.getSingleResult();
assertThat(findUser.getUsername()).isEqualTo("teasun");
}
여기서 잠깐!! 코드에 문자열이 들어가는게 왜 안좋은건가요?
(면접 질문 가능✔️)
- 문자열은 오타가 발생할 여지가 많다.
- 개발할때 같은 공통적인 문자열이 있을때 한군데에서 수정이 일어나면 모두 수정해야한다.
- 잘못된 코드가 있더라도 문자열 자체를 컴파일러가 검사 하지는 않기 때문에 컴파일 시점에 잡지못한다.
- 이로인해 버그가 있더라도 메소드를 실행하는 시점인 런타임시점에 버그가 발생한다.
- 런타임 시점에 발생한 버그는 서비스 정합성에 영향을 주며 원인을 찾기도 어렵다.
해결방법,
- 문자열을 포함하여 구현된 기능들은 객체화 또는 함수화 해서 컴파일시 체크되도록 한다.
- 문자열로 선언된 변수들은 상수로 선언하여 공통적으로 관리한다. (상수 클래스 선언 추천 👍)
1-2. @Qeury (repository interface)
- @Query 의 인자값으로 간단하게 쿼리를 작성할 수 있습니다.
- 쿼리를 작성할때는 테이블명이 아니라 Entity 명으로 조회하게 됩니다.
- 변수 바인딩은 2가지 방법으로 할 수 있습니다.
- ?변수순번 사용
public interface UserRepository extends JpaRepository<User, Long> {
// 아래와 같이 AS user_password 로 Alias(AS) 를 걸어주면
@Query("SELECT u, u.password AS customField FROM User u WHERE u.username = ?1")
List<User> findByUsernameWithCustomField(String username, Sort sort);
// 아래와 같이 AS user_password 로 Alias(AS) 를 걸어주면
@Query("SELECT u FROM User u WHERE u.username = ?1")
List<User> findByUsername(String username, Sort sort);
}
2. 변수 명 사용.
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u, u.password AS customField FROM User u WHERE u.username = :username")
List<User> findByUsernameWithCustomField(String username, Sort sort);
@Query("SELECT u FROM User u WHERE u.username = :username")
List<User> findByUsername(String username, Sort sort);
}
2. QueryDSL (JPAQueryFactory)
- QueryDSL 간단 정리!
- Entity 의 매핑정보를 활용하여 쿼리에 적합하도록 쿼리 전용 클래스(Q클래스) 로 재구성해주는 기술
- 여기에 JPAQueryFactory 을 통한 Q클래스를 활용할 수 있는 기능들을 제공한다.
- 그럼, JPAQueryFactory 는 뭐야?
- 재구성한 Q클래스를 통해 문자열이 아닌 객체 또는 함수로 쿼리를 작성하고 실행하게 해주는 기술
@PersistenceContext
EntityManager em;
public List<User> selectUserByUsernameAndPassword(String username, String password){
JPAQueryFactory jqf = new JPAQueryFactory(em);
QUser user = QUser.user;
List<Person> userList = jpf
.selectFrom(user)
.where(person.username.eq(username)
.and(person.password.eq(password))
.fetch();
return userList;
}
//
적용 하자면
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ThreadSearchCond {
private Long channelId;
private Long mentionedUserId; // 멘션된 유저 ID
}
public interface ThreadRepositoryQuery {
Page<Thread> search(ThreadSearchCond cond, Pageable pageable);
}
@RequiredArgsConstructor
public class ThreadRepositoryQueryImpl implements ThreadRepositoryQuery {
private final JPAQueryFactory jpaQueryFactory;
@Override
public Page<Thread> search(ThreadSearchCond cond, Pageable pageable) {
var query = query(thread, cond)
.offset(pageable.getOffset())
.limit(pageable.getPageSize());
query.orderBy(thread.mentions.any().createdAt.desc());
var threads = query.fetch();
long totalSize = countQuery(cond).fetch().get(0);
threads.stream()
.map(Thread::getComments)
.forEach(comments -> comments
.forEach(comment -> Hibernate.initialize(comment.getEmotions())));
return PageableExecutionUtils.getPage(threads, pageable, () -> totalSize);
}
private <T> JPAQuery<T> query(Expression<T> expr, ThreadSearchCond cond) {
return jpaQueryFactory.select(expr)
.from(thread)
.leftJoin(thread.channel).fetchJoin()
.leftJoin(thread.emotions).fetchJoin()
.leftJoin(thread.comments).fetchJoin()
.leftJoin(thread.mentions).fetchJoin()
.where(
channelIdEq(cond.getChannelId()),
mentionedUserIdEq(cond.getMentionedUserId())
);
}
private JPAQuery<Long> countQuery(ThreadSearchCond cond) {
return jpaQueryFactory.select(Wildcard.count)
.from(thread)
.where(
channelIdEq(cond.getChannelId()),
mentionedUserIdEq(cond.getMentionedUserId())
);
}
private BooleanExpression channelIdEq(Long channelId) {
return Objects.nonNull(channelId) ? thread.channel.id.eq(channelId) : null;
}
private BooleanExpression mentionedUserIdEq(Long mentionedUserId) {
return Objects.nonNull(mentionedUserId) ? thread.mentions.any().user.id.eq(mentionedUserId)
: null;
}
}