package study.querydsl.repository;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import study.querydsl.dto.MemberSearchCondition;
import study.querydsl.dto.MemberTeamDto;
import study.querydsl.dto.QMemberTeamDto;
import study.querydsl.entity.Member;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
import static study.querydsl.entity.QMember.member;
import static study.querydsl.entity.QTeam.team;
@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
private EntityManager em;
private JPAQueryFactory qf;
public Long save(Member member) {
em.persist(member);
return member.getId();
}
public Optional<Member> findById(Long id) {
return Optional.ofNullable(em.find(Member.class, id));
}
public Optional<Member> findById_query(Long id) {
return Optional.ofNullable(qf.selectFrom(member).where(member.id.eq(id)).fetchOne());
}
public List<Member> findByUsername(String username) {
return em.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class).setParameter("username", username).getResultList();
}
public List<Member> findByUsername_query(String username) {
return qf.selectFrom(member).where(member.username.eq(username)).fetch();
}
public List<Member> findAll() {
return em.createQuery("SELECT m FROM Member m", Member.class).getResultList();
}
public List<Member> findAll_query() {
return qf.selectFrom(member).fetch();
}
public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition) {
BooleanBuilder booleanBuilder = new BooleanBuilder();
if (StringUtils.hasText(condition.getUsername())) {
booleanBuilder.and(member.username.eq(condition.getUsername()));
}
if (StringUtils.hasText(condition.getTeamName())) {
booleanBuilder.and(team.name.eq(condition.getTeamName()));
}
if (condition.getAgeGeo() != null) {
booleanBuilder.and(member.age.goe(condition.getAgeGeo()));
}
if (condition.getAgeLoe() != null) {
booleanBuilder.and(member.age.loe(condition.getAgeLoe()));
}
return qf
.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(booleanBuilder)
.fetch();
}
public List<MemberTeamDto> searchMember(MemberSearchCondition condition) {
return qf
.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.getAgeGeo()),
ageLoe(condition.getAgeLoe())
)
.fetch();
}
private Predicate ageLoe(Integer ageLoe) {
return member.age.loe(ageLoe);
}
private Predicate ageGoe(Integer ageGeo) {
return member.age.goe(ageGeo);
}
private Predicate teamNameEq(String teamName) {
return StringUtils.hasText(teamName) ? team.name.eq(teamName) : null;
}
private Predicate usernameEq(String username) {
return StringUtils.hasText(username) ? member.username.eq(username) : null;
}
}
# src/main/resources/application.yml
# 로컬 실행 환경
spring:
profiles:
active: local
# src/main/test/resources/application.yml
# 테스트 환경
spring:
profiles:
active: test
App을 실행시키면 profile: local
로 설정된다.
Test 코드를 실행시키면 profile: test
로 설정된다.
로컬 환경에서만 작동하는 코드 만들기
@Profile("local")
@Component
@RequiredArgsConstructor
public class InitMember {
private final InitMemberService initMemberService;
@PostConstruct
public void init() {
initMemberService.init();
}
@Component
static class InitMemberSerivce {
@PersistenceContext
private EntityManager em;
@Transactional
public void init() {
// DB에 데이터 넣는 코드
}
}
}
간단한 기능은 JpaRepository
로 해결할 수 있는데, 동적 쿼리는 QueryDSL로 해결하는게 좋다.
사용자 정의 레포지토리
를 사용해서 JpaRepository와 QueryDSL을 합칠 수 있다.
// MemberRepositoryCustom (interface)
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
// MemberRepositoryImpl
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final JPAQueryFactory qf;
public MemberRepositoryImpl(EntityManager em) {
qf = new JPAQueryFactory(em);
}
@Override
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return qf
...
}
}
// MemberRepository (JpaRepository)
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
List<Member> findByUsername(String username);
}
만약 MemberRepositoryCustom
이 제공하는 기능이 범용적이지 않고 너무 특화(?)된 기능이라면, 사용자 정의 레포지토리
가 아닌 별도의 Repository Class로 구현한 뒤, Bean으로 등록해서 DI하는게 좋다.
Spring Data가 제공하는 Page
, Pageable
와 QueryDSL을 연동할 수 있다.
// MemberRepositoryCustom
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
Page<MemberTeamDto> searchPage(MemberSearchCondition condition, Pageable pageable);
}
// MemberRepositoryImpl
public class MemberRepositoryImpl implements MemberRepositoryCustom {
@Override
public Page<MemberTeamDto> searchPage(MemberSearchCondition condition, Pageable pageable) {
QueryResults<MemberTeamDto> results = qf
.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.getAgeGeo()),
ageLoe(condition.getAgeLoe())
)
.offset(pageable.getOffset()) // 시작 페이지 번호
.limit(pageable.getPageSize()) // 데이터 개수
.fetchResults();
List<MemberTeamDto> content = results.getResults();
long total = results.getTotal();
return new PageImpl<>(content, pageable, total);
}
앞서 살펴봤듯이,
getResults
는 페이징 데이터
, 총 개수
를 구하기 위해 쿼리가 두 번 발생한다.getResults
를 통해 발생한 총 개수
구하는 쿼리는 메인 쿼리
의 join, where문을 따라간다. getResults
는 deprecated페이징 데이터
와 총 개수
를 별도로 구하도록 하자.
@Override
public Page<MemberTeamDto> searchPage_simple(MemberSearchCondition condition, Pageable pageable) {
// 첫 번째 쿼리로 데이터만 불러온다.
List<MemberTeamDto> content = qf
.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.getAgeGeo()),
ageLoe(condition.getAgeLoe())
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
// 카운트는 명시적으로 별도의 쿼리를 발생시킨다.
long total = qf
.select(member)
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGeo()),
ageLoe(condition.getAgeLoe())
).fetchCount();
return new PageImpl<>(content, pageable, total);
}
...
JPAQuery<Member> countQuery = qf
.select(member)
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGeo()),
ageLoe(condition.getAgeLoe())
);
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);
Spring Data의 최적화를 사용해서, 상황에 따라 CountQuery를 발생시키지 않도록 할 수도 있다.
한 페이지에 보여줄 데이터가 20건인데, 검색 결과가 3건 밖에 없음.
어차피 1 페이지 밖에 없으니까, 총 페이지 개수를 개산하기 위한
'총 개수' 쿼리를 날릴 필요 없다.