BACK/JPA

[JPA] 실무 활용 - 스프링 데이터 JPA 와 Querydsl

연듀 2024. 3. 15. 21:19

스프링 데이터 JPA 리포지토리로 변경

 

public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByUsername(String username); // select m from Member m where m.username=?

}
@SpringBootTest
@Transactional
class MemberRepositoryTest {
    @Autowired
    EntityManager em;

    @Autowired MemberRepository memberRepository;

    @Test
    public void basicTest(){
        Member member = new Member("member1", 10);
        memberRepository.save(member);

        Member findMember = memberRepository.findById(member.getId()).get();
        assertThat(findMember).isEqualTo(member);

        List<Member> result1 = memberRepository.findAll();
        assertThat(result1).containsExactly(member);

        List<Member> result2 = memberRepository.findByUsername("member1");
        assertThat(result2).containsExactly(member);
    }

}

 

사용자 정의 리포지토리

 

  • 사용자 정의 리포지토리 사용법
  1. 사용자 정의 인터페이스 작성
  2. 사용자 정의 인터페이스 구현
  3. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속
public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(MemberSearchCondition condition);
}
public class MemberRepositoryImpl implements MemberRepositoryCustom{ // MemberRepository 라는 스프링데이터 인터페이스명  + Impl 규칙

    private final JPAQueryFactory queryFactory;

    public MemberRepositoryImpl(EntityManager em) {
        this.queryFactory =new JPAQueryFactory(em);
    }

    @Override
    public List<MemberTeamDto> search(MemberSearchCondition condition) {
        return queryFactory
                .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.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                // ageBetween(condition.getAgeLoe(), condition.getAgeGoe()))
                .fetch();
    }

    private BooleanExpression usernameEq(String username) {
        return hasText(username) ? member.username.eq(username) : null;
    }

    private BooleanExpression teamNameEq(String teamName) {
        return 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;
    }
}
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
    List<Member> findByUsername(String username); // select m from Member m where m.username=?

}

 

 

커스텀 리포지토리를 상속받지 않고,

조회용 기능을 화면에 맞게 분리해 내고 싶으면

별도의 조회용 리포지토리를 인터페이스가 아닌 클래스로 빼서 주입받아 쓰는 것도 좋은 방법이다.

 

 

스프링 데이터 페이징 활용1 - Querydsl 페이징 연동

public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(MemberSearchCondition condition);
    Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
    Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);

}

전체 카운트를 한번에 조회하는 단순한 방법

MemberRepositoryImpl

    @Override
    public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
        QueryResults<MemberTeamDto> results = queryFactory
                .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.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();// fetchResults() 하면 컨텐츠용 쿼리, 카운트 커리 두개 날린다.
        List<MemberTeamDto> content = results.getResults();
        long total = results.getTotal();

        return new PageImpl<>(content, pageable, total); // Page 의 구현체
    }

 

 

참고로 orderBy 같은 쿼리는 토탈 카운트 쿼리에서는 들어가지 않게 최적화해준다.

 

데이터 내용과 전체 카운트를 별도로 조회하는 방법

 @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);
    }

 

직접 카운트 쿼리를 날린다.

컨텐트 쿼리는 복잡한데 카운트 쿼리는 쉽게 만들 수 있는 경우가 있을 수 있다. (ex 조인을 없애기 등)

그런데 query dsl 에서 제공하는 fetchResults를 쓰면 다 조인을 하기 때문에 최적화를 못한다.

따로 fetchCount() 로 분리하면 최적화할 수 있다.

예를 들어 카운트 쿼리를 해서 카운트가 0이면 컨텐츠 쿼리를 아예 안하는 식으로 최적화할 수도 있다.

데이터가 별로 없을 땐 fetchResult, 데이터가 몇천만건 정도씩 많으면 고려하면 된다.

 

 

스프링 데이터 페이징 활용 2 - CountQuery 최적화

        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());

 

페이지의 시작이면서 컨텐츠 사이즈가 페이지의 사이즈보다 작거나, 마지막 페이지면 카운트 쿼리를 호출 안한다.

카운트 쿼리가 필요하면 날리고, 아니면 안날린다.

 

 

컨트롤러 개발

http://localhost:8080/v3/members?page=1&size=5

 

 

    @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);
    }

 

 

 

 

 

 

인프런 - 실전! Querydsl

https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84