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. 개발할때 같은 공통적인 문자열이 있을때 한군데에서 수정이 일어나면 모두 수정해야한다.
  3. 잘못된 코드가 있더라도 문자열 자체를 컴파일러가 검사 하지는 않기 때문에 컴파일 시점에 잡지못한다.
  4. 이로인해 버그가 있더라도 메소드를 실행하는 시점인 런타임시점에 버그가 발생한다.
  5. 런타임 시점에 발생한 버그는 서비스 정합성에 영향을 주며 원인을 찾기도 어렵다.

해결방법,

  • 문자열을 포함하여 구현된 기능들은 객체화 또는 함수화 해서 컴파일시 체크되도록 한다.
  • 문자열로 선언된 변수들은 상수로 선언하여 공통적으로 관리한다. (상수 클래스 선언 추천 👍) 

 

1-2. @Qeury (repository interface)

  • @Query 의 인자값으로 간단하게 쿼리를 작성할 수 있습니다.
    • 쿼리를 작성할때는 테이블명이 아니라 Entity 명으로 조회하게 됩니다.
  • 변수 바인딩은 2가지 방법으로 할 수 있습니다.
    1. ?변수순번 사용
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;
  }
}