@Repository
public class BookRepositorySupport extends QuerydslRepositorySupport {
private final JPAQueryFactory queryFactory;
public BookRepositorySupport(JPAQueryFactory queryFactory) {
super(Book.class);
this.queryFactory = queryFactory;
}
}
@RequiredArgsConstructor
@Repository
public class AcademyQueryRepository {
private final JPAQueryFactory queryFactory;
public List<Academy> findByName(String name) {
return queryFactory.selectFrom(academy)
.where(academy.name.eq(name))
.fetch();
}
}
@Override
public List<Academy> findDynamicQuery(String name, String address, String phoneNumber) {
BooleanBuilder builder = new BooleanBuilder();
if(!StringUtils.isEmpty(name)) {
builder.and(academy.name.eq(name));
}
if(!StringUtils.isEmpty(address)) {
builder.and(academy.address.eq(address));
}
if(!StringUtils.isEmpty(phoneNumber)) {
builder.and(academy.phoneNumber.eq(phoneNumber));
}
return queryFactory
.selectFrom(academy)
.where(builder)
.fetch();
}
@Override
public List<Academy> findDynamicQuery(String name, 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 academy.name.eq(name);
}
private BooleanExpression eqAddress(String address) {
if(StringUtils.isEmpty(address)) {
return null;
}
return academy.address.eq(address);
}
// SQL.exist - 2s 57ms,
select exists(
select 1
from ad_item_sum
where created_date > '2020-01-01')
// SQL.count(1) > 0 - 5s 208 ms
select count(1)
from ad_item_sum
where created_date > '2020-01-01'
@Override
public boolean exists(Predicate predicate) {
return createQuery(predicate).fetchCount() > 0;
}
return queryFactory.select(queryFactory
.selectOne()
.from(book)
.where(book.id.eq(bookId))
.fetchAll(.exists())
.fetchOne();
exists가 빠른 이유는 조건에 해당하는 row 1개만 찾으면 바로 쿼리를 종료하기 때문
가장 쉬운 방법
@Transactional(readOnly = true)
public Boolean exist(Long bookId) {
Integer fetchOne = queryFactory
.selectOne()
.from(book)
.where(book.id.eq(bookId))
// limit(1).fetchOne()과 동일
.fetchFirst();
return fetchOne != null;
}
public List<Cutomer> crossJoin() {
return queryFactory
.selectFrom(customer)
.where(customer.customerNo.gt(customer.shop.shopNo))
.fetch();
}
public List<Cutomer> crossJoin() {
return queryFactory
.selectFrom(customer)
.innerJoin(customer.shop, shop)
.where(customer.customerNo.gt(customer.shop.shopNo))
.fetch();
}
일부 블로그들.. 해당 영상 예전 블로그 포스팅 참조해서 꼭 명시적으로 InnerJoin() 메서드를 써야한다고 하는데.. 아닙니다...
Querydsl crossJoin() 메서드 없음
join 메서드가 innerJoin으로 동작
queryFactory
.selectFrom(book)
.where(book.bookNo.eq(bookNo))
.offset(pageNo)
.limit(10)
.fetch();
queryFactory
.selectFrom(Projections.fields(BookPageDto.class,
book.name,
book.bookNo,
book.id))
.from(book)
.where(book.bookNo.eq(bookNo))
.offset(pageNo)
.limit(10)
.fetch();
public List<BookPageDto> getBooks (int bookNo, int pageNo) {
return queryFactory
.select(Projections.fields(BookPageDto.class,
book.name,
book.bookNo,
book.id
))
.from(book)
.where(book.bookNo.eq(bookNo))
.offset(pageNo)
.limit(10)
.fetch();
public List<BookPageDto> getBooks (int bookNo, int pageNo) {
return queryFactory
.select(Projections.fields(BookPageDto.class,
book.name,
// as 표현식으로 대체할 수 있음
// as 컬럼은 select에서 제외된다.
Expressions.asNumber(bookNo).as("bookNo"),
book.id
))
.from(book)
.where(book.bookNo.eq(bookNo))
.offset(pageNo)
.limit(10)
.fetch();
return queryFactory
.select(Projections.fields(AdBond.class,
adItem.amount.sum().as("amount"),
adItem.txDate,
adItem.orderType,
// adItem과 연결된 Customer 조회
// Customer의 모든 컬럼 조회(사용하지 않는 컬럼임에도..)
adItem.customer)
)
....
Customer와 @OneToOne 관계인 Shop이 매 건마다 조회한다.
Shop에도 @OneToOne인 컬럼이 있다면??!
Entity간 연관관계를 맺으려면 반대 Entity가 있어야하지 않나요?
return queryFactory
.select(Projections.fields(AdBond.class,
adItem.amount.sum().as("amount"),
adItem.txDate,
adItem.orderType,
// customerId만 조회
adItem.customer.id.as("customerId")
)
....
public AdBond toEntity() {
return AdBond.Builder()
.amount(amount)
.txDate(txDate)
.orderType(orderType)
.customer(new Customer(customerId))
.build();
}
성능 비교 : 2500만건 기준
엔티티 조회는 실제 필요한지를 고려하여 사용하길 추천
public class OrderByNull extends OrderSpecifier {
public static final OrderByNull DEFAULT = new OrderByNull();
private OrderByNull() {
super(Order.ASC, NullExpression.DEFAULT, Default);
}
}
.groupBy(txAddition.type, txAddition.code)
.orderBy(OrderByNull.DEFAULT)
.fetch();
result.sort(comparingLong(PointCalculateAmount::getPointAmount));
// 1. 속성들을 가져올 때
select *
from academy a
// 2. 커버링 인덱스를 사용함
// -> JPQL에서 안되니까 PK를 커버링 인덱스로 먼저 가져오고 속성들 가져오는 방식으로 우화
join (select id
from academy
order by id
limit 10000, 10) as temp
on temp.id = a.id;
// 1. PK 먼저 가져오고
List<Long> ids = queryFactory
.select(book.id)
.from(book)
.where(book.name.like(name + "%"))
.orderBy(boo.id.desc())
.limit(pageSize)
.offset(pageNo * pageSize)
.fetch();
if (CollectionUtils.isEmpty(ids)) {
return new ArrayList<>();
}
// 2. 나머지 속성들 가져오기
return queryFactory
.select(Projections.fields(BookPaginationDto.class,
book.id.as("bookId"),
book.name,
book.bookNo,
book.bookType))
.from(book)
.where(book.id.in(ids))
.orderBy(book.id.desc())
.fetch();
List<Student> students = queryFactory
.selectFrom(student)
.where(student.id.loe(studentId))
.fetch();
for (Student student : students) {
student.updateName(name);
}
queryFactory.update(student)
.where(student.id.loe(studentId))
.set(student.name, name)
.execute();
DirtyChecking이란?
성능 : 1만건 기준 (약 2,000배 차이)
공통 내용 : 진짜 Entity가 필요한게 아니라면 Querydsl과 Dto를 통해 딱 필요한 항목들만 조회하고 업데이트한다.
성능 : 단일 Entity 1만건 save 기준
rewriteBatchedStatements로 Insert 합치기 옵션을 넣어도 JPA는 auto_increment일때 insert 합치기가 적용되지 않는다.
JdbcTemplate으로 Bulk Insert는 처리되나 컴파일 체크, 코드-테이블간 불일치 체크 등 Type Safe 개발이 어려움
일부만 적용하고 있으니 걸러서 듣기..!
- 로컬 PC에 DB 설치 및 실행
- Gradle/Maven에 로컬 DB 정보를 등록
- flyway로 테이블 생성
- Querydsl-SQL 플러그인으로 테이블 Scan 하여 QClass 생성
EntityQL로 만들어진 Querydsl-SQL의 QClass를 이용하면 BulkInsert 가능!
단일 Entity
SQLInsertClause insert = sqlQueryFactory.insert(qAcademy);
for (int j = 1; j <= 1_000; j++) {
insert.populate(new Academy("address", "name"), EntityMapper.DEFAULT)
.addBatch();
}
insert.execute();
SQLInsertCluase insert = sqlQueryFactory.insert(qStudent);
for (int j = 1; j <= 1_000; j++) {
Academy = academy = academyRepository.save(new Academy("address", "name"));
insert.populate(new Student("student", 1, academy), EntityMapper.DEFAULT).addBatch();
insert.populate(new Student("student", 2, academy), EntityMapper.DEFAULT).addBatch();
}
insert.execute();
Gradle 5이상 필요
어노테이션에 (name = "") 필수
primitive type 사용 X
복잡한 설정
@Embedded 미지원
Querydsl-SQL의 미지원으로 insert 쿼리를 @Column의 name으로 만들 수가 없음