QueryDsl은 객체 지향적 쿼리 작성 프레임워크이기에 매우 편리하고,
컴파일 시점에서 오류를 잡아준다.
또한 가독성이 매우 좋고, 동적 쿼리 작성도 매우 좋다.
✔️ JPAQueryFactory
QueryDsl이 제공하는 JPQL 툴이다.
이를 통하여 쿼리를 작성한다.
사용할 때마다 생성하는 것보다 빈에 등록하여 사용하는 것이 좋다.
Config 클래스를 만들어서 빈으로 등록하고 의존관계를 주입받아 사용하자.
@Configuration
@RequiredArgsConstructor
public class QuerydslConfiguration {
private final EntityManager em;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(em);
}
}
✔️ DTO 조회
이전에 스프링 데이터 JPA나 JPA에서 DTO 생성자를 통한 직접 조회를 했을 때
패키지명을 다 적어야해서 매우 불편했다.
QueryDsl은 @QueryProjection 어노테이션을 제공하여 DTO 클래스도 Q타입의 클래스를 생성해준다.
이로써 패키지명을 적을 필요없이 간단하게 Q타입으로 생성자를 지정하면 된다.
@Data
public class MemberDto {
private String username;
private int age;
public MemberDto() {
}
@QueryProjection // DTO도 Q타입의 클래스로 생성하여 사용
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final JPAQueryFactory queryFactory;
public List<MemberDto> findAllAsDto() {
return queryFactory
.select(new QMemberDto(
member.username,
member.age
))
.from(member)
.fetch();
}
}
✔️ 동적 쿼리
Querydsl에서 동적쿼리는
1. BooleanBuilder 사용
2. where 다중 파라미터 사용
이 있다.
1. BooleanBuilder 사용
@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
private final JPAQueryFactory queryFactory;
public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition) {
BooleanBuilder builder = new BooleanBuilder();
// null 과 "" 을 체크
if (hasText(condition.getUsername())) {
builder.and(member.username.eq(condition.getUsername()));
}
if (hasText(condition.getTeamName())) {
builder.and(team.name.eq(condition.getTeamName()));
}
if (condition.getAgeGoe() != null) {
builder.and(member.age.goe(condition.getAgeGoe()));
}
if (condition.getAgeLoe() != null) {
builder.and(member.age.loe(condition.getAgeLoe()));
}
return queryFactory
.select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName")
))
.from(member)
.leftJoin(member.team, team)
.where(builder)
.fetch();
}
}
1. builder를 생성
2. 동적 조건을 if문으로 처리
3. 조건이 있다면 builder에 and로 조건을 추가
2. where 다중 파라미터 사용
public List<MemberTeamDto> searchByWhere(MemberSearchCondition condition) {
return queryFactory
.select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName")
))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageBetWeen(condition.getAgeLoe(), condition.getAgeGoe()))
.fetch();
}
// 조합
private BooleanExpression ageBetWeen(Integer ageLoe, Integer ageGoe) {
return ageLoe(ageLoe).and(ageGoe(ageGoe));
}
private BooleanExpression usernameEq(String username) {
return hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName) {
return hasText(teamName) ? team.name.eq(teamName) : null;
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe != null ? member.age.goe(ageGoe) : null;
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe != null ? member.age.loe(ageLoe) : null;
}
BooleanExpression은 Predicate 타입을 구현했다.
조건을 조합할 수도 있고, 재사용성도 매우 높으며 null을 반환하면 where절에서 무시한다.
강추!
참고
hasText : StringUtils 클래스의 메소드
✔️ 벌크 연산
벌크 update - set를 지원
단, 영속성 컨텍스트를 무시하고 DB에 다이렉트로 update하니
1차캐시에는 반영되지 않음
따라서 기존 영속성 컨텍스트에 있는 엔티티가 업데이트 된 것을 보려면
영속성 컨텍스트를 초기화하고 다시 조회해야함.
queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(20))
.execute();
참고로 delete도 지원한다.
ps. SQLFunction, function()도 지원한다. 자세한건 필요할때 찾아서 사용하자.
✔️ Custom으로 스프링 데이터 JPA와 결합
QueryDsl과 스프링 데이터 JPA를 결합하여 하나로 사용해보자.
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
이를 구현한 클래스
@Repository
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final JPAQueryFactory queryFactory;
// querydsl 로 작성한 쿼리메서드들..
}
- 규칙 : 리포지토리 인터퍼이스 이름 || 사용자 정의 인터페이스 이름 + Impl
- 스프링 데이터 JPA가 인식해서 스프링 빈으로 등록
// 이 인터페이스 이름이 리포지토리 이름이다.
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
스프링 데이터 JPA에서 Custom을 상속하면 된다.
✔️ Page 기능
page 기능도 가능하다.
PageImpl 을 생성하여 만들면 된다.
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
List<MemberTeamDto> content = queryFactory
.select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName")
))
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
Long total = queryFactory
.select(member.count())
.from(member)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
).fetchOne();
return new PageImpl<>(content, pageable, total);
}
생성자의 파라미터는
1. content : 말 그대로 페이징할 데이터 리스트
2. pageable : 페이지 정보
3. total : 전체 데이터의 개수
PageableExcutionUtils를 사용하면 마지막 페이지는 count쿼리를 날리지 않는다.
// totalCount가 페이지 사이즈 보다 적거나, 마지막 페이지 일 경우 Count쿼리를 실행하지 않는다
JPAQuery<Long> countQuery = queryFactory
.select(member.count())
.from(member)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
);
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
기존 page 쿼리에서 이렇게만 바꾸면 된다.
✔️ NoOffset 사용
사실 페이징 시 문제가 있다면 그것은 DB에서 1~1000000까지를 페이징한다 했을 때
처음 1~20까지 조회하는 것은 빠르다. 하지만
50만~50만20 조회부터는 느려진다.
그 이유는 DB에서 offset을 적용할 때 50만20 까지 읽은 다음 뒤에 50만을 버리고 20을 반환한다.
이때문에 성능적으로 매우 비효율적임을 알 수 있다.
따라서 offset을 사용하지 않고 페이징한 마지막 조회 데이터의 pk값을 받아서 이를 조건식에 넣어 조회 성능을 높인다.
기본적으로 pk는 클러스터링 인덱스 이기에 매우 빠르게 조회가 가능하다.
- 클러스터링 인덱스는 데이터가 실제(물리적)로 저장되는 순서와 인덱스 (논리적) 의 순서가 일치하는 인덱스입니다.
여기서는 내림차순으로 최신데이터를 먼저 보여주기에 Lt(less than)을 사용한다.
member.id < 마지막 데이터 pk
@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final JPAQueryFactory queryFactory;
public List<MemberDto> noOffset(Long lastMemberId, int limit) {
return queryFactory
.select(new QMemberDto(
member.username,
member.age
))
.from(member)
.where(member.username.contains("member")
.and(memberIdLt(lastMemberId)))
.orderBy(member.id.desc())
.limit(limit)
.fetch();
}
private BooleanExpression memberIdLt(Long lastMemberId) {
return lastMemberId != null ? member.id.lt(lastMemberId) : null;
}
}
여기서는 간단하게 했지만
Slice를 구현하여 무한스크롤을 구현할 수 있다.
🔖 학습내용 출처
김영한. (2021). 자바 ORM 표준 JPA 프로그래밍. 에이콘출판사