실무 활용 - 순수 JPA 와 Querydsl

31400 단어 JPASpringQuerydslJPA

순수 JPA 리포지토리와 Querydsl

MemberJpaRespotiroy

package study.querydsl.repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.hibernate.EntityNameResolver;
import org.springframework.stereotype.Repository;
import study.querydsl.entity.Member;
import study.querydsl.entity.QMember;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

import static study.querydsl.entity.QMember.*;

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

    public List<Member> findAll_Querydsl() {
        return queryFactory
                .selectFrom(member)
                .fetch();
    }

    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_Querydsl(String username) {
        return queryFactory
                .selectFrom(member)
                .where(member.username.eq(username))
                .fetch();
    }
}

MemberJpaRepository 생성자에서 JPAQueryFactory를 새로 만들어 엔티티 매니저를 주입시키는 것을 볼 수 있다.
이 방법 말고도 JPAQueryFactory를 빈으로 따로 만들고 생성자에 그냥 주입만 시키는 방법도 있다.

이 경우에는 @RequiredArgsConstructor를 통해 더 간편한 코드를 짤 수 있다는 장점이 있다.
하지만, 이 방법은 테스트를 진행할때 외부에서 가져와야한다는 단점이 있다고 말씀하셨으나 이해를 잘 못했다.

참고
동시성 문제의 경우, JPAQueryFactory는 엔티티 매니저에 의존을 하게 되는데, 엔티티 매니저는 실제 동작 시점에 진짜 엔티티 매니저를 찾아주는 프록시용 가짜 엔티티 매니저이다. 실제 사용 시점에 트랜잭션 단위로 실제 엔티티 매니저를 할당해준다.

이에 대한 자세한 내용은 자바 ORM 표준 JPA 책 13.1 트랜잭션 범위의 영속성 컨텍스트를 참고.

Test

동적 쿼리와 성능 최적화 조회 - Builder 사용

MemberTeamDto

결과값을 받을 Dto이다. @QueryProjection를 이용하여 GradlecompileQuerydsl을 실행한 결과인 QMemberTeamDto를 이용한다.

이 방법은 편리하다는 장점을 가지고 있지만, Dto가 쿼리dsl 라이브러리를 의존하고, Q클래스를 생성한다는 단점을 가지고 있다.

다른 방법을 사용하고 싶다면, 중급문법에서 Querydsl을 이용한 Dto 변환을 참조하여 3가지 방법중에 하나를 선택하면 된다.

MemberSearchCondition

필터 조건을 넣을 클래스이다.
username이고, teamName이면서, ageGoe < age < ageLoe 조건인 age를 가지고 있는 멤버를 조회하여 Dto에 담을 것이다.

MemberJpaRepository

StringUtils.hasText() : null"" 인 경우면 false
값이 있다면 그 조건에 맞춰 eq, goe, loe 등 조건을 빌더에 넣어준다.
이 빌더는 where구문에 넣어준다.

Test

위 테스트의 경우 팀 이름teamB 인 멤버들을 찾는 것이다.
다른 조건(나이, 이름 등)에 값을 넣어주면 해당 조건이 추가된다.

중요

이 구조의 경우 조건에 아무것도 넣지않으면 모든 데이터를 전부 가져온다.
DB의 모든 데이터를 가져오는 것은 운영할때는 안좋을 수 있다.
그러므로 초기 조건을 미리 세팅하던지, (default)
limit 등을 이용해 한계를 설정해주는 것이 좋다.

동적 쿼리와 성능 최적화 조회 - Where절 파라미터 사용

MemberJpaRepository

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

이 방식의 장점은
파라미터 메서드들을 전부 재사용할 수 있다는 점이다.
또, 이 메서드들을 조합한 새로운 메서드를 생성하여 더 간편화하게 코드를 작성할 수 있다.
그 외에도 BooleanBuilder와 다르게 바로 코드를 확인할 수 있다는 장점을 가지고 있다.

조회 API 컨트롤러 개발

이렇게 서버와 테스트를 구분하기 위해 이름을 다르게 짓는다.

InitMember

package study.querydsl.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import study.querydsl.entity.Member;
import study.querydsl.entity.Team;

import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Profile("local")
@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));
            }
        }
    }
}

@Profile("local")
local이라는 이름의 프로파일이 있으면 실행된다.
이름이 다르면 이 어노테이션에 있는 것들이 등록이 안되는 것 같다.

@PostConstruct를 통해 초기에 생성한다.

MemberController

테스트를 위한 컨트롤러이다.
파라미터로 조건들을 넣으면 조건에 맞게 검색이 된다.

teamName = teamB
31 < age < 35
username = member31

teamName = teamB
31 < age < 35

그리고 테스트에서는 위 프로파일 이름이 test이므로,
@Profile("local")이 빈에 등록되지 않아서
데이터가 초기에 만들어지지 않는다.
즉, 테스트용 데이터를 따로 만들기 위해 테스트와 서버를 나눠서 만들었다고 생각하면 될 것 같다.

좋은 웹페이지 즐겨찾기