우아콘 QueryDSL 요약
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// queryDSL
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
// QClass 생성을 위한 annotaionProcessor
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}JPARepository 선언
public interface ExampleRepository extends JpaRepository<Example, Long> {
}
JPAQueryFactory 빈 등록
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory(){
return new JPAQueryFactory(entityManager)
}
}
CustomRepository interface 생성
선언한 인터페이스 구현 (반드시 인터페이스 이름 뒤 Impl을 붙여 구현)
사용하려는 Repository에서 extends로 CustomRepository 상속받아 사용
Entity가 정말 필요한 것이 아니면, QueryDsl과 Dto만으로 필요한 항목만 조회하고 업데이트한다.
@Repository
@RequireArgsContstructor
public class QueryRepository {
private final JPAQueryFactory queryFactory;
// QueryDsl 메서드 작성
}BooleanBuilder를 사용하는 경우 어떤 쿼리인지 예상하기 어려움@Override
public List<Member> findDynamicQuery(Stirng nmae, String address, String phoneNumber){
BooleanBuilder builder = new BooleanBuilder();
if(!StringUtils.isEmpty(name)){
builder.and(member.name.eq(name));
}
if(!StringUtils.isEmpty(address)){
builder.and(member.address.eq(address));
}
if(!StringUtils.isEmpty(phoneNumber)){
builder.and(member.phoneNumber.eq(phoneNumber));
}
return queryFactory
.selectFrom(academy)
.where(builder)
.fetch();
}→ BooleanExpression 을 이용하여 조건을 메서드 단위로 분리
@Override
public List<Member> findDynamicQuery(Stirng nmae, String address, String phoneNumber){
return queryFactory
.selectFrom(academy)
.where(eqName(name),
eqAddress(address),
eqPhoneNumber(phoneNumber))
.fetch();
}
private BooleanExpression eqName(String name) {
if(StringUtils.isEmpty(name){
return null;
}
return member.name.eq(name);
}
private BooleanExpression eqAddress(String address) {
if(StringUtils.isEmpty(address){
return null;
}
return member.address.eq(address);
}
결과가 없는 경우 null이기 때문에 0으로 비교하지 않고 null인지를 판별
@Transactional(readOnly = true)
public Boolean exist(Long memberId){
Integer fetchOne = queryFactory
.selectOne()
.from(member)
.where(mebmer.id.eq(mebmerId))
.fetchFirst();
return fetchOne != null;
}
→ 명시적 Join을 이용한 Inner/Outer Join 사용
public List<Customer> notCrossJoin(){
return queryFactory
.selectFrom(customer)
.innerJoin(customer.shop, shop)
.where(customer.customerNo.gt(shop.shopNo))
.fetch();
}
Entity 조회
→ 실시간으로 Entity 변경이 필요한 경우
Dto 조회
→ 고강도 성능 개선 or 대량의 데이터 조회가 필요한 경우
4-1 조회 컬럼 최소화하기
- 이미 알고 있는 값은 조회하지 않음
- as 표현식을 이용하면 select에서 제외 가능
public List<BookPageDto> getBooks (int bookNo, int pageNo) { return queryFactory .select( Projections.fields( BookPageDto.class, book.name, Expressions.asNumber(bookNo).as("bookNo"), book.id )) .from(book) .where(book.bookNo.eq(bookNo)) .offset(pageNo) .limit(10) .fetch(); }
4-2 Select 컬럼에 Entity 사용 자제
- oneToOne은 Lazy Loading이 안됨 → N + 1이 무조건 발생
- 연관된 Entity의 save를 위해서는 반대편 Entity ID만 있으면 됨
- insert 기준으로 반대편 Entity Id만 조회하여 저장하면 됨
- distinct 문제 발생
→ select에 선언된 Entity 컬럼 전체가 대상이 되므로 문제 발생
4-3 Group By 최적화
- MySQL에서는 Group By를 실행하면 Filesort가 필수로 발생 (Index가 아닌 경우)
- order by null을 사용하면 Filesort가 제거됨
- 그러나 QueryDSL에서는 해당 문법을 지원하지 않음
→ 우회를 통한 최적화 필요public class OrderByNull extends OrderSpecifier { public static final OrderByNull DEFAULT = new OrderByNull(); private OrderByNull(){ super(Order.ASC, NullExpression.DEFAULT, Default); } } // .orderBy(OrderByNull.DEFAULT)
- 정렬이 필요해도, 조회 결과가 100건 이하인 경우 애플리케이션에서 정렬
- DB보다 WAS의 자원이 저렴하여 WAS가 자원적으로 여유로움
- 페이징의 경우 order by null을 사용하지 못하므로 페이징이 아닌 경우에만 사용
DirtyChecking
List<Student> students = queryFactory .selectFrom(student) .where(student.id.loe(studentId)) .fetch(); for(Student student : students){ student.updateName(name); }→ 실시간 비지니스처리, 실시간 단건 처리의 경우
Querydsl.update
queryFactory.update(student) .where(student.id.loe(studentId)) .set(student.name, name) .execute();→ Hibernate Cache는 일괄 Update시 Cache 갱신이 안되어 업데이트 대상들에 대한 Cache Evicition이 필요
→ 대량의 데이터의 일괄 Update가 필요한 경우