[Spring] SpringStudy-6일차
JPQL을 이용할 떄의 검색
단일 엔티티가 아니라 QueryDSL에서 필요한 쿼리를 직접 실행할 구조가 필요
Repository를 확장해서 JPQLQuery를 이용해 직접 JPQL을 생성해서 처리.
Repository에 search 패키지 추가하고
인터페이스와 Impl파일 추가.
test {
useJUnitPlatform()
}
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
configurations {
querydsl.extendsFrom compileClasspath
}
쿼리dsl을 사용하기 위해 build.gradle에 설정.
@Repository를 확장하기 위해
쿼리 메서드나 @Query로 처리 할 수 없는 기능은 별도의 인터페이스로 설계
- 별도의 인터페이스에 대한 구현클래스를 작성. 이때 QueryRepositorySupport라는 클래스를 부모로 사용.
- 구현 클래스에 Q도메인 클래스와 JPQLQuery를이용.
QueryRepositorySupport 참고 : https://awse2050.tistory.com/26
이 클래스느 ㄴJPA에 포함된 클래스로 Querydsl라이브러리로 직접 뭔가 구현할때 사용.
repository에 search 패키지 작성.
추가된 search 패키지에 확장하고 싶은 기능을 인터페이스로 선언후 Board 타입 객체 반환하는 메서드 선언
public interface SearchBoardRepository {
Board search1();
Page<Object[]> searchPage(String type, String keyword, Pageable pageable);
}
이 인터페이스를 구현하기 위한 SearchBoardRepositoryImpl에서 QuerydslRepositorySupport클래스를 상속해야함. 이 클래스는 생성자가 존재해서 super()를 이용해서 호출해야하고 이때 도메인 클래스를 지정하는 데 null값을 넣을 수는 없음.
정상동작은 BoardRepositoryTest로 확인
2021-05-12 01:50:48.121 INFO 22632 --- [ Test worker] o.z.b.r.s.SearchBoardRepositoryImpl : search1........................
search 로그가 찍힘.
위와같이 로그가 기록되는게 확인되면 실제 JPQL을 작성하고 실행해보는 단계로 감.
이 과정에서 Querydsl의 라이브러리 내 JPQLQuery라는 인터페이스 활용.
활용하기 위해 코드 설정
QBoard board = QBoard.board;
QReply reply = QReply.reply;
QMember member = QMember.member;
JPQLQuery<Board> jpqlQuery = from(board);
jpqlQuery.leftJoin(member).on(board.writer.eq(member));
jpqlQuery.leftJoin(reply).on(reply.board.eq(board));
JPQLQuery<Tuple> tuple = jpqlQuery.select(board, member.email, reply.count());
tuple.groupBy(board);
log.info("---------------------------");
log.info(tuple);
log.info("---------------------------");
List<Tuple> result = tuple.fetch();
log.info(result);
return null;
가장 중요한게 from이용
select 했을 때 객체 자체가 쿼리 자체 넣는거.(JPQL자체를 구문으로 만들어냄.
실제 밑에서 fetch로 결과를 가져옴.
select board, member1.email, count(reply)
from Board board
left join Member member1 with board.writer = member1
left join Reply reply with reply.board = board
group by board
이와 같이 바꿔줌. 그리고 Board형 객체 대신 각각의 데이터를 추출하는 경우 Tuple을 이용해서 수정하는게 좋음. (위 코드가 엔티티 객체인 Board 대신 Tuple사용한 코드)
select() 결과를 JPQLQuery
-> 다 가져오는게 아니라 부분적으로 가져오고 싶을때 튜플 사용한다 생각 하면 됨.
목록 만들고 페이지 처리 해줘야함
파라미터 Pageable을 전송하고 Page<Object[]>를 반환
SelectBoardRepository에 파라미터와 리턴타입을 반영하는 searchList()설계
public interface SearchBoardRepository {
Board search1();
Page<Object[]> searchPage(String type, String keyword, Pageable pageable);
}
searchPage()에서 검색타입, 키워드 페이지 정보를 파라미터로 받아왔는데, pageRequestDTO자체를 파라미터로 처리하지 않는 이유는DTO를 가능하면 Repository영역에서 다루지 않기 때문
public Page<Object[]> searchPage(String type, String keyword, Pageable pageable) {
log.info("searchPage.............................");
QBoard board = QBoard.board;
QReply reply = QReply.reply;
QMember member = QMember.member;
JPQLQuery<Board> jpqlQuery = from(board);
jpqlQuery.leftJoin(member).on(board.writer.eq(member));
jpqlQuery.leftJoin(reply).on(reply.board.eq(board));
//SELECT b, w, count(r) FROM Board b
//LEFT JOIN b.writer w LEFT JOIN Reply r ON r.board = b
JPQLQuery<Tuple> tuple = jpqlQuery.select(board, member, reply.count());
BooleanBuilder booleanBuilder = new BooleanBuilder();
BooleanExpression expression = board.bno.gt(0L);
booleanBuilder.and(expression);
if(type != null){
String[] typeArr = type.split("");
//검색 조건을 작성하기
BooleanBuilder conditionBuilder = new BooleanBuilder();
for (String t:typeArr) {
switch (t){
case "t":
conditionBuilder.or(board.title.contains(keyword));
break;
case "w":
conditionBuilder.or(member.email.contains(keyword));
break;
case "c":
conditionBuilder.or(board.content.contains(keyword));
break;
}
}
booleanBuilder.and(conditionBuilder);
}
tuple.where(booleanBuilder);
여기 까지 하면 목록 생성한 게 확인 됨
Hibernate:
select
board0_.bno as col_0_0_,
member1_.email as col_1_0_,
count(reply2_.rno) as col_2_0_,
board0_.bno as bno1_0_0_,
...
from
board board0_
left outer join
tbl_member member1_
on (
board0_.writer_email=member1_.email
)
left outer join
reply reply2_
on (
reply2_.board_bno=board0_.bno
)
where
board0_.bno>?
and (
board0_.title like ? escape '!' //제목으로 검색되는 조건이 추가되었음
)
group by
board0_.bno
pageable의 sort객체는 jpqlQuery의 order파라미터로 전달되어야 하지만 jpql에선 sort를 지원하지 않아서 OrderSpecifier
OrderSpecifier에서 order,Expression은 각각 com.querydsl.core타입이고
org.springframework.data.domain.Sort는 내부적으로 여러 sort객체 연결할 수 있어 foreach()로 처리.
OrderSpecifier엔 정렬이 필요해서 Sort객체의 정렬 관련 정보를 Order타입으로 처리하고 Sort객체에 속성(bno, title)등은 PathBuilder로 처리
//order by
Sort sort = pageable.getSort();
//tuple.orderBy(board.bno.desc());
sort.stream().forEach(order -> {
Order direction = order.isAscending()? Order.ASC: Order.DESC;
String prop = order.getProperty();
PathBuilder orderByExpression = new PathBuilder(Board.class, "board");
tuple.orderBy(new OrderSpecifier(direction, orderByExpression.get(prop)));
});
tuple.groupBy(board);
//page 처리
tuple.offset(pageable.getOffset());
tuple.limit(pageable.getPageSize());
List<Tuple> result = tuple.fetch();
log.info(result);
long count = tuple.fetchCount();
log.info("COUNT: " +count);
return new PageImpl<Object[]>(
result.stream().map(t -> t.toArray()).collect(Collectors.toList()),
pageable,
count);
}
JPQLQUery를 이용해서 동적으로 검색 조건을 처리해보면 복잡하지만 한번의 개발로 count쿼리도 같이 처리 가능(다른 쿼리도 같이 처리)
검색처리는 PageRequestDTO로 이전 예제에서 검색에 쓰던 action 속성을 board/list로 변경하는거 제외하면 별 다른 작업 필요하지 않음. 컨트롤러 역시 동일함.
// Page<Object[]> result = repository.getBoardWithReplyCount(
// pageRequestDTO.getPageable(Sort.by("bno").descending()) );
Page<Object[]> result = repository.searchPage(
pageRequestDTO.getType(),
pageRequestDTO.getKeyword(),
pageRequestDTO.getPageable(Sort.by("bno").descending()) );
BoardServiceImpl은 주석부분을 위와같이 바꿔주면 됨.