- Today
- Total
개발하는 고라니
[JPA] Querydsl 기본 본문
Querydsl
Querydsl은 정적 타입을 이용해서 SQL과 같은 쿼리를 생성할 수 있도록 해주는 프레임워크이다. 또한 Querydsl은 JPQL 빌더이다.
현재 JPA를 JPQL -> Spring Data JPA -> Querydsl순으로 배우고 있다. Querydsl은 무엇이고 왜 쓰이는지, 어떻게 쓰는지 JPQL과 비교하며 알아본다.
JPQL을 사용하다보면 한계에 부딪힐 때가 있다.
1) 복잡한 쿼리
- JPQL에서 복잡한 쿼리를 타이핑 중 오타가 날 수도 있고, 가독성이 떨어질 수 있다.
- JPQL에서 파라미터를 .setParameter(name, value)를 통해 바인딩해야한다.
- Querydsl에서 java 코드를 사용하기 때문에 컴파일 시점에 타입 체크나 오타를 잡아주어 IDE의 도움을 받을 수 있다. 반면 JPQL는 문자열로 쿼리를 짜기 때문에 컴파일 시점에 디버깅이 불가능하다. 물론 요즘은 IDE가 좋아져 일부 JPQL의 에러는 IDE의 도움을 받을 수 있다.
- Querydsl에서 파라미터를 바인딩 할 때 바로 꽂아 넣을 수 있어 간편하고 가독성이 좋다.
예) User, Board, Reply가 있다고 할 때, 댓글을 조회하는데 댓글의 내용과 게시글의 제목, 댓글을 쓴 사람의 이름으로 필터링 해서 조회하는 쿼리를 짜보자.
> JPQL
@Test
@DisplayName("댓글 조회 테스트_JPQL")
void read_replies_test() throws Exception {
//given
String username = "name";
String title = "title";
String content = "content";
//when
String query = "select r "
+ "from Reply r "
+ "left join fetch r.board b "
+ "left join fetch r.user u "
+ "left join fetch r.parent p "
+ "where ( "
+ "b.title = :title "
+ "and u.name = :username "
+ "and r.content = :content "
+ " ) "
+ "order by r.id desc";
List<Reply> replies = em.createQuery(query, Reply.class)
.setParameter("username", username)
.setParameter("title", title)
.setParameter("content", content)
.getResultList();
//then
}
> Querydsl
@Test
@DisplayName("댓글 조회 테스트_Querydsl")
void read_replies_test_querydsl() throws Exception {
//given
String username = "name";
String title = "title";
String content = "content";
//when
QReply parent = new QReply("p");
List<Reply> replies = factory
.selectFrom(reply)
.leftJoin(reply.board, board).fetchJoin()
.leftJoin(reply.user, user).fetchJoin()
.leftJoin(reply.parent, parent).fetchJoin()
.where(
user.name.eq(username),
board.title.eq(title),
reply.content.eq(content)
)
.orderBy(reply.id.desc())
.fetch();
//then
}
개인적으로 둘 다 mybatis를 이용할 때보다 훨씬 편한 것은 맞지만, querydsl을 써보니 확실히 더 편하고, 깔끔한 것을 느낀다.
2) 동적 쿼리
- JPQL에서 동적 쿼리를 사실상 쓰기가 불가능하다고 해도 과언이 아니다. Criteria라는 것을 이용해야하는데, 이걸 사용하라고 만든 건지 의문이 들을 정도로 복잡하다. 그래서 써본적도 없고 앞으로도 쓸 일은 없을 것이다.
- Querydsl은 BooleanBuilder나 다중 파라미터 등을 이용해 동적쿼리를 손쉽게 사용할 수 있다.
> Querydsl
@Test
@DisplayName("동적 쿼리")
void dynamicQuery_test() throws Exception {
//given
String username = "name";
String title = "title";
String content = "content";
//when
QReply parent = new QReply("p");
List<Reply> replies = factory
.selectFrom(reply)
.leftJoin(reply.board, board).fetchJoin()
.leftJoin(reply.user, user).fetchJoin()
.leftJoin(reply.parent, parent).fetchJoin()
.where(
dynamicSearch(username, title, content)
)
.orderBy(reply.id.desc())
.fetch();
//then
}
private BooleanBuilder dynamicSearch(String username, String title, String content) {
BooleanBuilder builder = new BooleanBuilder();
if(username != null){
builder.and(user.name.eq(username));
}
if(title != null){
builder.and(board.title.eq(title));
}
if(content != null){
builder.and(reply.content.eq(content));
}
return builder;
}
따라서 이러한 문제들을 간단하고 깔끔하게 해결할 수 있는 것이 Querydsl이다. 요약하자면, Querydsl은 쿼리를 자바 코드로 작성해 컴파일 시점에 문법 오류를 잡아주며, 동적 쿼리 문제를 해결할 수 있고, 쉬운 SQL 스타일의 문법으로 쉽게 학습할 수 있다.
참고사항
※ Querydsl을 사용하기 위해선 JPAQueryFactory를 사용해야하는데, 이를 각 메서드에서 생성해 사용하는 것보다 Class의 필드로 선언해놓고 쓰는 것이 권장된다. 멀티 스레드환경에서도 동시성 문제가 발생하지 않게 구현되어 있기 때문에 이는 걱정하지 않아도 된다고 한다.
class SampleQuerydslRepository {
@PersistenceContext
private EntityManager em;
private JPAQueryFactory factory;
...
}
※ Querydsl은 특이하게 Q-Type을 사용하는데, 이를 static import 해놓고 사용하면 편하다.
import gorany.dslshop.entity.QBoard;
import gorany.dslshop.entity.QReply;
import gorany.dslshop.entity.QUser;
...
※ 앞으로의 예제에 사용될 도메인 간의 관계이다.
조회
- fetch() : List 조회, 데이터가 없으면 빈 List 반환
- fetchOne() : 단 건 조회
- 결과가 없을 때 : null
- 결과가 2개 이상일 때 : NonUniqueResultException 발생
- fetchFirst() : limit(1).fetchOne()
- fetchResults() : 페이징 정보를 포함하여 조회되며, 전체 개수가 필요하기 때문에 count query가 실행된다.
- fetchCount() : count 쿼리로 변경해서 count 수 조회
//fetch
String query = "select u from User u";
List<User> users_jpql = em.createQuery(query, User.class)
.getResultList();
List<User> users_querydsl = factory
.selectFrom(user)
.fetch();
//fetchOne
User user_jpql = em.createQuery(query, User.class)
.getSingleResult();
User user_querydsl = factory
.selectFrom(QUser.user)
.fetchOne();
//fetchFirst
User user_jpql = em.createQuery(query, User.class)
.setMaxResults(1)
.getSingleResult();
User user_querydsl = factory
.selectFrom(QUser.user)
.fetchFirst();
//fetchResults
/*
복잡한 쿼리의 경우 fetchResults()를 사용하지 말고,
데이터를 가져오는 것과 count를 가져오는 쿼리를 다르게 써야할 수도 있다.
(성능 문제)
*/
//JPQL은 안되고 Spring Data JPA를 써야 비슷하게 가능
QueryResults<User> users_querydsl = factory
.selectFrom(user)
.fetchResults();
//fetchCount
long count_jpql = em.createQuery("select count(u) from User u", Long.class)
.getSingleResult();
long count_querydsl = factory
.selectFrom(user)
.fetchCount();
페이징
페이징의 경우 복잡한 것 말고 비교적 간단한 쿼리를 작성해서, 카운트 쿼리와 조회 쿼리를 동일하게 사용한다는 가정을 한다.
> JPQL
@Test
@DisplayName("paging JPQL")
void paging_jpql_test() throws Exception {
//given
String content = "reply%";
//when
List<Reply> result = em.createQuery("select r from Reply r "
+ "join fetch r.user u "
+ "join fetch r.board b "
+ "where r.content like :content ", Reply.class)
.setParameter("content", content)
.setFirstResult(0)
.setMaxResults(10)
.getResultList();
//then
assertThat(result.size()).isEqualTo(10);
assertThat(result.get(0).getContent()).contains("reply");
}
> Querydsl
> fetch
@Test
@DisplayName("paging")
void paging_test() throws Exception {
//given
String content = "reply";
//when
List<Reply> result = factory
.selectFrom(reply)
.join(reply.user, user).fetchJoin()
.join(reply.board, board).fetchJoin()
.where(reply.content.startsWith(content))
.offset(0)
.limit(10)
.fetch();
//then
assertThat(result.size()).isEqualTo(10);
assertThat(result.get(0).getContent()).contains("reply");
}
> fetchResults
@Test
@DisplayName("paging fetchResults")
void paging_fetchResults_test() throws Exception {
//given
String content = "reply";
//when
QueryResults<Reply> result = factory
.selectFrom(reply)
.join(reply.user, user).fetchJoin()
.join(reply.board, board).fetchJoin()
.where(reply.content.startsWith(content))
.offset(0)
.limit(10)
.fetchResults();
//then
assertThat(result.getTotal()).isEqualTo(50);
assertThat(result.getResults().get(0).getContent()).contains("reply");
}
조인
조인에는 inner join, left/right join이 있으며, 연관관계가 없는 필드로 조인하는 '세타 조인'이 있다. JPQL의 on과, 성능 최적화를 위한 fetch join을 지원한다.
//inner fetch join
List<Board> result_inner = factory
.selectFrom(board)
.innerJoin(board.user, user).fetchJoin()
.fetch();
//left fetch join
List<Board> result_left = factory
.selectFrom(board)
.leftJoin(board.user, user).fetchJoin()
.fetch();
//inner join
List<Board> result_inner = factory
.selectFrom(board)
.innerJoin(board.user, user)
.fetch();
//left join
List<Board> result_left = factory
.selectFrom(board)
.leftJoin(board.user, user)
.fetch();
세타 조인
하다보면 연관관계가 없는 데이터에 대해서도 조인해서 사용해야할 때가 있을 수 있다. (난 아직없었다)
이를 해결할 수 있는 방법이 2가지 정도 있다. 하나는 where 절에서 처리하는 것이고, 나머지는 join에 'on'을 사용하는 것이다.
> where 절 (cross join)
※ 이때 주의할 사항은 from 절에 여러 엔티티를 선택해야한다.
@Test
@DisplayName("theta join")
void theta_join_where_test() throws Exception {
//given
//when
//댓글의 내용과 게시글의 내용이 같은 것을 조회한다.
List<Reply> result = factory
.select(reply)
.from(reply, board)
.where(reply.content.eq(board.content))
.fetch();
//then
/*
* 실행된 SQL
select
reply0_.reply_id as reply_id1_3_,
reply0_.board_id as board_id3_3_,
reply0_.content as content2_3_,
reply0_.parent_id as parent_i4_3_,
reply0_.user_id as user_id5_3_
from
reply reply0_ cross
join
board board1_
where
reply0_.content=board1_.content
* */
}
> on 사용 (inner join 혹은 left/right join)
@Test
@DisplayName("using on join")
void using_join_on_test() throws Exception {
//given
//when
List<Tuple> result = factory
.select(reply, board)
.from(reply)
//이 부분을 잘 봐야한다. leftJoin()에 하나의 엔티티만 들어간다.
//leftJoin(reply.board, board) ==> 이건 ID로 ON절이 매핑되어있다.
.leftJoin(board).on(reply.content.eq(board.content))
.fetch();
//then
result.forEach(System.out::println);
/*
* 실행된 SQL
select
reply0_.reply_id as reply_id1_3_0_,
board1_.board_id as board_id1_0_1_,
reply0_.board_id as board_id3_3_0_,
reply0_.content as content2_3_0_,
reply0_.parent_id as parent_i4_3_0_,
reply0_.user_id as user_id5_3_0_,
board1_.content as content2_0_1_,
board1_.title as title3_0_1_,
board1_.user_id as user_id4_0_1_
from
reply reply0_
left outer join
board board1_
on (
reply0_.content=board1_.content
)
* */
}
※ ON은 이외에도 조인 대상 필터링을 할 수 있다. 예를 들어 reply와 board를 조회하는데, board의 title이 "board1"인 것만 조회해보자.
> on을 이용해 조인 대상 필터링
@Test
@DisplayName("조인 대상 필터링 using on")
void filtering_using_on() throws Exception {
//given
String boardTitle = "board1";
//when
List<Tuple> result = factory
.select(reply, board)
.from(reply)
.leftJoin(reply.board, board).on(board.title.eq(boardTitle))
.fetch();
//then
result.forEach(System.out::println);
/*
* 실행된 SQL
select
reply0_.reply_id as reply_id1_3_0_,
board1_.board_id as board_id1_0_1_,
reply0_.board_id as board_id3_3_0_,
reply0_.content as content2_3_0_,
reply0_.parent_id as parent_i4_3_0_,
reply0_.user_id as user_id5_3_0_,
board1_.content as content2_0_1_,
board1_.title as title3_0_1_,
board1_.user_id as user_id4_0_1_
from
reply reply0_
left outer join
board board1_
on reply0_.board_id=board1_.board_id
and (
board1_.title=?
)
* */
/*
* 출력 결과
[Reply(id=1, content=reply0), Board(id=1, title=board1, content=board_content1)]
[Reply(id=2, content=reply1), null]
[Reply(id=3, content=reply2), null]
[Reply(id=4, content=reply3), null]
[Reply(id=5, content=reply4), null]
[Reply(id=6, content=reply5), null]
[Reply(id=7, content=reply6), null]
[Reply(id=8, content=reply7), null]
...
* */
}
※ 사실 on으로 조인 대상을 필터링할 때 inner join을 쓰면 where을 사용하는 것과 동일하다.
@Test
@DisplayName("filtering using where")
void filtering_using_where() throws Exception {
//given
//when
List<Tuple> result = factory
.select(reply, board)
.from(reply)
.innerJoin(reply.board, board)
.where(board.title.eq("board1"))
.fetch();
//then
result.forEach(System.out::println);
/*
* 실행된 SQL
select
reply0_.reply_id as reply_id1_3_0_,
board1_.board_id as board_id1_0_1_,
reply0_.board_id as board_id3_3_0_,
reply0_.content as content2_3_0_,
reply0_.parent_id as parent_i4_3_0_,
reply0_.user_id as user_id5_3_0_,
board1_.content as content2_0_1_,
board1_.title as title3_0_1_,
board1_.user_id as user_id4_0_1_
from
reply reply0_
inner join
board board1_
on reply0_.board_id=board1_.board_id
where
board1_.title=?
* */
/*
* 출력 결과
[Reply(id=1, content=reply0), Board(id=1, title=board1, content=board_content1)]
[Reply(id=9, content=reply8), Board(id=1, title=board1, content=board_content1)]
[Reply(id=17, content=reply16), Board(id=1, title=board1, content=board_content1)]
[Reply(id=25, content=reply24), Board(id=1, title=board1, content=board_content1)]
[Reply(id=33, content=reply32), Board(id=1, title=board1, content=board_content1)]
[Reply(id=41, content=reply40), Board(id=1, title=board1, content=board_content1)]
[Reply(id=49, content=reply48), Board(id=1, title=board1, content=board_content1)]
* */
}
#References
'Framework > JPA (Hibernate)' 카테고리의 다른 글
[JPA] Spring Data JPA (0) | 2021.10.09 |
---|---|
[JPA] OSIV (Open Session In View) (2) | 2021.09.19 |
[JPA] 벌크 연산 (Bulk Operation) (0) | 2021.08.05 |
[JPA] 페치 조인 (FETCH JOIN) (0) | 2021.08.05 |
[JPA] Cascade / OrphanRemoval (0) | 2021.07.25 |