사용 코드
- MemberTeamDto.java : 반환할 Dto클래스
- MemberSearchCondition.java : 검색할 필드가 들어있는 클래스
- MemberJpaRepository.java : 리포지토리
- MemberController.java : API 컨트롤러
- InitMember.java : 애플리케이션 구동시 데이터 등록할 클래스
MemberTeamDto.java
@Data
public class MemberTeamDto {
private Long memberId;
private String username;
private int age;
private Long teamId;
private String teamName;
@QueryProjection //gradle에서 compileQuerydsl을 해줘서 Q파일 생성해야함
public MemberTeamDto(Long memberId, String username, int age, Long teamId, String teamName) {
this.memberId = memberId;
this.username = username;
this.age = age;
this.teamId = teamId;
this.teamName = teamName;
}
}
MemberSearchCondition.java
@Data
public class MemberSearchCondition {
//회원명, 팀명, 나이(ageGoe 크거나 같다>=, ageLoe 작거나 같다<=)
private String username;
private String teamName;
private Integer ageGoe;
private Integer ageLoe;
}
MemberJpaRepository.java
@Repository
public class MemberJpaRepository {
private final EntityManager em;
private final JPAQueryFactory queryFactory;
public MemberJpaRepository(EntityManager em) {
this.em = em;
this.queryFactory = new JPAQueryFactory(em);
}
public void save(Member member) {
em.persist(member);
}
public Optional<Member> findById(Long id) {
Member findMember = em.find(Member.class, id);
return Optional.ofNullable(findMember);
}
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
//querydsl 사용
public List<Member> findAll_Querydsl() {
return queryFactory
.selectFrom(member)
.fetch();
}
public List<Member> findByName(String username) {
return em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", username)
.getResultList();
}
//querydsl 사용
public List<Member> findByName_Querydsl(String username) {
return queryFactory
.selectFrom(member)
.where(member.username.eq(username))
.fetch();
}
//동적 쿼리(Builder 사용)
//회원명, 팀명, 나이
public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition) {
BooleanBuilder builder = new BooleanBuilder();
//StringUtils.hasTest => null, "" 체크
if (StringUtils.hasText(condition.getUsername())) {
builder.and(member.username.eq(condition.getUsername()));
}
if (StringUtils.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,
member.username,
member.age,
team.id,
team.name))
.from(member)
.leftJoin(member.team, team)
.where(builder)
.fetch();
//필드 접근
// return queryFactory
// .select(Projections.fields(MemberTeamDto.class,
// member.id.as("memberId"),
// member.username,
// member.age,
// team.id.as("teamId"),
// team.name.as("teamName")))
// .from(member)
// .join(member.team, team)
// .where(builder)
// .fetch();
}
//동적 쿼리(Where절에 파라미터 사용)
public List<MemberTeamDto> searchByWhere(MemberSearchCondition condition) {
return queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.fetch();
}
private Predicate usernameEq(String username) {
return StringUtils.hasText(username) ? member.username.eq(username) : null;
}
private Predicate teamNameEq(String teamName) {
return StringUtils.hasText(teamName) ? team.name.eq(teamName) : null;
}
private Predicate ageGoe(Integer ageGoe) {
return ageGoe != null ? member.age.goe(ageGoe) : null;
}
private Predicate ageLoe(Integer ageLoe) {
return ageLoe != null ? member.age.loe(ageLoe) : null;
}
}
MemberController.java
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberJpaRepository memberJpaRepository;
@GetMapping("/v1/members")
public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition) {
return memberJpaRepository.searchByWhere(condition);
}
}
InitMember.java
@Profile("local") //이 설정을 하면 application.yml에서 profile과 동일한 이름을 찾아 실행한다.
@Component
@RequiredArgsConstructor
public class InitMember {
private final InitMemberService initMemberService;
@PostConstruct
public void init() {
initMemberService.init();
}
@Component
static class InitMemberService {
@PersistenceContext
private EntityManager em;
@Transactional
public void init() {
Team teamA = new Team("teamA");
Team teamB= new Team("teamB");
em.persist(teamA);
em.persist(teamB);
for (int i = 0; i < 100; i++) {
Team selectedTeam = i % 2 == 0 ? teamA : teamB;
em.persist(new Member("member"+i, i, selectedTeam));
}
}
}
}
Postman 호출 결과
※ 참 고
InitMember.java를 보면 @Profile 어노테이션이 있는데 사용한 이유는
테스트 코드 작성시에도 초기 데이터가 등록되는 것을 방지하는 목적으로 메인에서만 실행되도록 구분한 것이다.
사용 방법은 application.yml 파일에 설정을 해주면 된다.
//메인 application.yml
spring:
profiles:
active: local //테스트의 application.yml은 test로 작성
datasource:
url: jdbc:h2:tcp://localhost/~/querydsl
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
# show_sql: true
format_sql: true
use-sql-comments: true
logging.level:
org.hibernate.SQL: debug
# org.hibernate.type: trace
위와 같이 설정하면 애플리케이션 구동시에 아래와 같이 local이 출력된다.
사용 코드
- MemberTeamDto.java : 1번에 있던 소스 동일
- MemberSearchCondition.java : 1번에 있던 소스 동일
- MemberRepositoryCustom.java : 사용자 정의 리포지토리 인터페이스
- MemberRepositoryCustomImpl.java : 사용자 정의 리포지토리 인터페이스 구현체
- MemberRepository.java : 리포지토리
- MemberController.java : API 컨트롤러
MemberRepositoryCustom.java
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
// 컨텐츠와 카운트를 함께 조회하는 fetchResults()사용
// Querydsl이 제공하는 fetchResults()는 내용과 전체 카운트를 한번에 조회
Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
// 컨텐츠와 카운트 쿼리 분리
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
MemberRepositoryCustomImpl.java
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom{
private final JPAQueryFactory queryFactory;
public MemberRepositoryCustomImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
@Override
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.fetch();
}
private BooleanExpression usernameEq(String username) {
return StringUtils.hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName) {
return StringUtils.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;
}
@Override
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
QueryResults<MemberTeamDto> results = queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name))
.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())
.fetchResults();//컨텐츠와 카운트 함께 조회
List<MemberTeamDto> content = results.getResults();
long total = results.getTotal();
return new PageImpl<>(content, pageable, total); //Pageable의 구현체 PageImpl
}
/**
* 복잡한 페이징
* 데이터 조회 쿼리와 전체 카운트 쿼리를 분리
*/
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
List<MemberTeamDto> content = queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name))
.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)
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.fetchCount();
return new PageImpl<>(content, pageable, total);
}
}
MemberRepository.java
//JpaRepository 인터페이스와 사용자 정의 리포지토리인 MemberRepositoryCustom인터페이스 추가
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
//select m from Member m where m.username = :username
List<Member> findByUsername(String username);
}
MemberController.java
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberJpaRepository memberJpaRepository;
private final MemberRepository memberRepository;
@GetMapping("/v2/members")
public Page<MemberTeamDto> searchMemberV2(MemberSearchCondition condition, Pageable pageable) {
return memberRepository.searchPageSimple(condition, pageable);
}
@GetMapping("/v3/members")
public Page<MemberTeamDto> searchMemberV3(MemberSearchCondition condition, Pageable pageable) {
return memberRepository.searchPageComplex(condition, pageable);
}
}
※ 추 가(페이징의 CountQuery 최적화)
위의 MemberRepositoryCustomImpl.java에서 searchPageComplex메서드 부분을 최적화 할 수 있다.
/*long total = queryFactory
.select(member)
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.fetchCount();*/
JPAQuery<Member> countQuery = queryFactory
.select(member)
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()));
// return new PageImpl<>(content, pageable, total);
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);
설명
PageableExecutionUtils.getPage()로 페이지를 최적화 하는 것으로
스프링 데이터 라이브러리가 제공하며 count 쿼리가 생략 가능한 경우 생략한다.
- 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때
- 마지막 페이지 일 때(offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함)
ex)페이지 사이즈는 5인데 데이터가3건일 경우 count쿼리는 생성 안됨
[6] QueryDSL [1/2] (0) | 2021.07.20 |
---|---|
[5] 스프링 데이터 JPA (0) | 2021.07.08 |
[4] JPA 활용2 강의 내용 [2/2] (0) | 2021.06.17 |
[2] 자바 ORM 표준 JPA 프로그래밍 - 기본편(객체지향 쿼리) [2/2] (0) | 2021.05.20 |
[1] 자바 ORM 표준 JPA 프로그래밍 - 기본편 [1/2] (0) | 2021.05.04 |