테이블이 아닌 엔티티 객체를 대상으로 검색
SQL을 추상화해서 특정 데이터베이스 SQL에 의존X
em.createQuery("select m from Member m where m.age > 18", Member.class)
=> 단점: 문자라서 컴파일 오류가 발생하지 않음
=>TypeQuery<>: 반환 타입이 명확할 때 사용, Query: 반환 타입이 명확하지 않을 때 사용
파라미터 바인딩
SELECT m FROM Member m where m.username=:username
//페이징 쿼리
String jpql = "select m from Member m order by m.name desc";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setFirstResult(10)
.setMaxResults(20)
.getResultList();
예) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
JPQL:
SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
JPQL:
SELECT m, t FROM
Member m LEFT JOIN Team t on m.username = t.name
JPA는 select
, where
, having
절에서만 서브쿼리 사용가능
[NOT] EXISTS : 서브쿼리에 결과가 존재하면 참
ALL 모두 만족하면 참
ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참
[NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
• 이가 평균보다 많은 회원
select m from Member m
where m.age > (select avg(m2.age) from Member m2)
• 한 건이라도 주문한 고객
select m from Member m
where (select count(o) from Order o where m = o.member) > 0
• A 소속인 회원
select m from Member m
where exists (select t from m.team t where t.name = ‘팀A')
• 전체 상품 각각의 재고보다 주문량이 많은 주문들
select o from Order o
where o.orderAmount > ALL (select p.stockAmount from Product p)
• 어떤 팀이든 팀에 소속된 회원
select m from Member m
where m.team = ANY (select t from Team t)
// queryDSL 설정
implementation "com.querydsl:querydsl-jpa"
implementation "com.querydsl:querydsl-core"
implementation "com.querydsl:querydsl-collections"
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa" // 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 설정부
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)
}
※JPAQueryFactory 등록 (bean으로 등록)
@Bean
JPAQueryFactory jpaqueryfactory(EntityManager em) {
Return new JPAQueryFactory(em);
//JPQL
//select m from Member m where m.age > 18
JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member;
List<Member> list =
query.selectFrom(m)
.where(m.age.gt(18))
.orderBy(m.name.desc())
.fetch();
offset
과 limit
를 통해서 페이징을 할 수 있다. 중요한 것이 offset은 시작 페이지를 정하는 것이 아니라 시작 row를 정하는 것이다Pageable 인터페이스는 PageRequest.of(page, size, sort)
로 구현해서 넘겨주면, 따로 코드 구현 없이 인수를 넘겨주는 것만으로도 페이징 혹은 정렬이 된다!
=>결과를 DTO로 변환해서 반환할 것
ex) return memberRepository.findAll(pageable).map(MemberDto::new);
Pageable
의 반환 타입 : Sort에 페이징 기능까지 추가된 것이다.
①Page<> : 추가 count 쿼리 발생, 현재 페이지에 대한 정보와, 전체 데이터의 개수 조회
②Slice<> : 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적으로 limit +1을 조회하여 다음 페이지가 있는지 없는지만 확인)
결론은 Spring Data JPA + QueryDSL로 페이징 처리하면 됨
Spring Data JPA와 QueryDSL를 함께 사용하기 위해서는 사용자 정의 Repository
를 정의해야한다.
QueryDSL을 사용하여 검색, 페이징 처리 등의 결과 값을 받을 때, 객체가 아닌 DTO로 받으려면 Dto생성자에 @QueryProjection
을 사용
@QueryProjection
를 사용하면 객체로 값을 받은 후 Dto클래스로 변환하는 과정 없이 QDto를 생성
해서 바로 Dto객체를 뽑을 수 있음
public interface ItemRepositoryCustom {
Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto,
Pageable pageable);
/*
상품 검색키워드을 담고 있는 ItemSearchDto 객체와 페이징 정보를 담고있는
Pageable 객체를 파라미터로 받는 메서드를 정의
*/
public class ItemRepositoryCustomImpl implements ItemRepositoryCustom{
private JPAQueryFactory queryFactory; //동적으로 쿼리를 생성하기 위해 필요
public ItemRepositoryCustomImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em); //생성자로 EntityManager를 넣어줌줌
}
private BooleanExpression searchSellStatusEq(ItemSellStatus searchSellStatus){
return searchSellStatus == null ? null : item.itemSellStatus.eq(searchSellStatus);
//조건이 null이라서 null이 반환되면 해당 검색조건은 무시된다.
}
private BooleanExpression regDtsAfter(String searchDateType){
LocalDateTime dateTime = LocalDateTime.now();
if (StringUtils.equals("all", searchDateType) || searchDateType == null) {
return null;
}else if (StringUtils.equals("1d", searchDateType)){
dateTime = dateTime.minusDays(1); // 시간을 현재부터 1일전으로 세팅
}else if (StringUtils.equals("1w", searchDateType)){
dateTime = dateTime.minusWeeks(1);
}else if (StringUtils.equals("1m", searchDateType)){
dateTime = dateTime.minusMonths(1);
}else if (StringUtils.equals("6m", searchDateType)){
dateTime = dateTime.minusMonths(6);
}
return item.regTime.after(LocalDate.from(dateTime)); // 해당 시간 이후로 등록된 상품
}
private BooleanExpression searchByLike(String searchBy, String searchQuery){ //검색조건, 검색어
if (StringUtils.equals("itemNm", searchBy)){
return item.itemNm.like("%" + searchQuery + "%");
} else if (StringUtils.equals("createdBy" , searchBy)) {
return item.createdBy.like("%" + searchQuery + "%");
}
return null;
}
@Override
public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {
List<Item> content = queryFactory.selectFrom(item)
.where(searchSellStatusEq(itemSearchDto.getItemSellStatus()))
.where(regDtsAfter(itemSearchDto.getSearchDateType()))
.where(searchByLike(itemSearchDto.getSearchBy(), itemSearchDto.getSearchQuery()))
.orderBy(item.id.desc())
.offset(pageable.getOffset()) // 데이터를 가지고 올 시작 인덱스를 지정
.limit(pageable.getOffset()) // 한 번에 가지고 올 최대 개수 지정
.fetch();
Long total = queryFactory.select(Wildcard.count).from(item)
.where(regDtsAfter(itemSearchDto.getSearchDateType()),
searchSellStatusEq(itemSearchDto.getItemSellStatus()),
searchByLike(itemSearchDto.getSearchBy(), itemSearchDto.getSearchQuery()))
.fetchOne();
return new PageImpl<>(content, pageable, total);
}
}
Custom인터페이스를 사용자정의 Repository가 extend한다.
-QueryDSL을 사용해서 동적쿼리 등을 처리할 때 주로 구현
Service에서 메서드 활용
controller에서 PageRequest.of로 Pageable을 생성