QueryDSL Part.3

dev_314·2023년 4월 4일
0

JPA - Trial and Error

목록 보기
16/16

순수 JPA + QueryDSL

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에 데이터 넣는 코드
        }
    }
}

Spring Data JPA + QueryDSL

간단한 기능은 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);
    }

앞서 살펴봤듯이,

  1. getResults페이징 데이터, 총 개수를 구하기 위해 쿼리가 두 번 발생한다.
  2. getResults를 통해 발생한 총 개수 구하는 쿼리는 메인 쿼리의 join, where문을 따라간다.
    • 즉, 단순히 총 개수만 구하는 쿼리인데 불필요한 join, where문이 발생할 수도 있다.
  3. 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);
    }

개선2

	...
	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 페이지 밖에 없으니까, 총 페이지 개수를 개산하기 위한 
'총 개수' 쿼리를 날릴 필요 없다. 
profile
블로그 이전했습니다 https://dev314.tistory.com/

0개의 댓글