[QueryDSL] EP4. 실무활용-순수 JPA 리포지토리와 Querydsl
Web/QueryDSL

[QueryDSL] EP4. 실무활용-순수 JPA 리포지토리와 Querydsl

 

 

 

순수 JPA 리포지토리와 Querydsl

 

순수 JPA 리포지토리에 Querydsl을 적용해 봅니다.

 

  1. 리포지토리를 만들고 EntityManager와 JPAQueryFactory를 선언합니다.
  2. 생성자주입을 통해 스프링으로부터 EntityManager를 주입받습니다.
  3. JPAQueryFactory에 EntityManager를 넣어 생성합니다.
    (QueryFactory는 EntityManager에 의존하게됩니다.)

 

@Repository
public class MemberJpaRepository {

    private final EntityManager em;
    private final JPAQueryFactory queryFactory;

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

}

 

 

 

순수 JPA로 된 리포지토리의 메서드 중 findAll과 findByUsername을 Querydsl로 바꿔봅니다.

    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }


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

 

 

 

 

Querydsl로 바꿨을때의 장점

 

  1. JPQL과 달리 컴파일 시점에 오류검사를 해줍니다.
  2. 파라미터 바인딩을 인자로 넣는 방식으로 구현합니다.

 

 

 

 

JPAQueryFactory를 스프링 빈으로 등록하기

 

 

Application에서 JPAQueryFactory를 스프링 빈으로 등록해 리포지토리에서 생성자 주입으로 받을 수도 있습니다.

 

package study.querydsl;

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import javax.persistence.EntityManager;

@SpringBootApplication
public class QuerydslApplication {

	public static void main(String[] args) {
		SpringApplication.run(QuerydslApplication.class, args);
	}

	@Bean
	JPAQueryFactory jpaQueryFactory(EntityManager em) {
		return new JPAQueryFactory(em);
	}

}

 

이렇게 되면 리포지토리에서 스프링을 통해 생성자로 주입받고 @Requ 로 생략가능합니다.

package study.querydsl.repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
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
@RequiredArgsConstructor
public class MemberJpaRepository {

    private final EntityManager em;
    private final JPAQueryFactory queryFactory;

//    public MemberJpaRepository(EntityManager em, JPAQueryFactory queryFactory) {
//        this.em = em;
//        this.queryFactory = queryFactory;
//    }

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


}

 

 

 

 

 

동시성문제

JPAQueryFactory에 엔티티 매니저를 넣어 생성합니다.

그래서 QueryFactory는 엔티티매니저에 의존하게됩니다.

엔티티매니저가 트랜잭션 단위로 분리돼서 동작하기 때문에 동시성문제가 해결 됩니다.

 

 

 

 

 

 

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

 

 

조건이 null 이 들어갈 수 있는 동적쿼리를 구현하기 위해 실무에서 Builder를 사용해봅니다.

 

 

    public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition) {

        BooleanBuilder builder = new BooleanBuilder();
        if (hasText(condition.getUsername())) {
            builder.and(member.username.eq(condition.getUsername()));
        }
        if (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.as("memeerId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")
                ))
                .from(member)
                .where(builder)
                .leftJoin(member.team, team)
                .fetch();
    }

 

 

테스트 요구사항

35살이상, 40살 이하이고 teamB에 속한 member

 

    @Test
    public void searchTest() throws Exception {
        //given
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);
        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);
        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        //when
        MemberSearchCondition condition = new MemberSearchCondition();
        condition.setAgeGoe(35);
        condition.setAgeLoe(40);
        condition.setTeamName("teamB");

        List<MemberTeamDto> result = memberJpaRepository.searchByBuilder(condition);

        //then

        assertThat(result).extracting("username").containsExactly("member4");
    }

 

 

 

 

이제 동적쿼리가 잘 작동하는지 몇가지 조건을 빼고 테스트를 해봅니다.

 

나이조건을 넣지 않고 teamB 조건만 넣어서 결과를 보겠습니다.

    @Test
    public void searchTest() throws Exception {
        //given
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);
        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);
        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        //when
        MemberSearchCondition condition = new MemberSearchCondition();
//        condition.setAgeGoe(35);
//        condition.setAgeLoe(40);
        condition.setTeamName("teamB");

        List<MemberTeamDto> result = memberJpaRepository.searchByBuilder(condition);

        //then

//        assertThat(result).extracting("username").containsExactly("member4");
        assertThat(result).extracting("username").containsExactly("member3", "member4");
    }

 

 

 

where에 나이조건은 빠지고 team이름 조건만 들어간 것을 볼 수 있습니다.

 

 

 

 

"무"조건 문제

만약 조건을 아예 넣지 않고 다 빼버리면 어떻게 되는지 확인해 봅니다.

 

    @Test
    public void searchTest() throws Exception {
        //given
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);
        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);
        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        //when
        MemberSearchCondition condition = new MemberSearchCondition();
//        condition.setAgeGoe(35);
//        condition.setAgeLoe(40);
//        condition.setTeamName("teamB");

        List<MemberTeamDto> result = memberJpaRepository.searchByBuilder(condition);

        //then
        for (MemberTeamDto memberTeamDto : result) {
            System.out.println("memberTeamDto = " + memberTeamDto);
        }
//        assertThat(result).extracting("username").containsExactly("member4");
//        assertThat(result).extracting("username").containsExactly("member3", "member4");
    }

 

 

 

team을 left join하고 모든 member를 가져옵니다.

 

 

 

대응방안

데이터 수가 많아지면 "무"조건 요청시 성능이 저하되기 때문에 웬만하면 기본조건이 있는것이 좋습니다.

아니면 limit라도 걸어주어야합니다.

 

 

 

 

 

 

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

 

 

 

위와 동일한 조건의 동적쿼리를 querydsl의 Where절 파라미터를 이용해 실무에 적용해보겠습니다.

 

    public List<MemberTeamDto> search(MemberSearchCondition condition) {
        return queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memeerId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")
                ))
                .from(member)
                .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;
    }
    @Test
    public void searchTest1() throws Exception {
        //given
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);
        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);
        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);

        //when
        MemberSearchCondition condition = new MemberSearchCondition();
        condition.setAgeGoe(35);
        condition.setAgeLoe(40);
        condition.setTeamName("teamB");

        List<MemberTeamDto> result = memberJpaRepository.search(condition);

        //then

        assertThat(result).extracting("username").containsExactly("member4");
    }

 

 

 

 

장점

 

1. projection이 달라져도 재사용 가능

 

만약 MemberTeamDto가 아닌 Member로 반환하고 싶다면 select 절만 Member로 바꿔주면 됩니다.

    public List<Member> searchMember(MemberSearchCondition condition) {
        return queryFactory
                .selectFrom(member)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                        )
                .fetch();
    }

 

 

 

 

 

2. null만 조심하면 조건 메서드들을 조립해 사용가능

 

    public List<Member> searchMember(MemberSearchCondition condition) {
        return queryFactory
                .selectFrom(member)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
//                        ageGoe(condition.getAgeGoe()),
//                        ageLoe(condition.getAgeLoe())
                        ageBetween(condition.getAgeLoe(), condition.getAgeGoe())
                        )
                .fetch();
    }

    private BooleanExpression ageBetween(int ageLoe, int ageGoe) {
        return ageGoe(ageLoe).and(ageLoe(ageGoe));
    }


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

 

 

 

 

 

 

 

 

 

조회 API 컨트롤러 개발

 

샘플 데이터 추가

편리한 데이터 확인을 위해 샘플데이터를 추가할 것입니다

하지만 샘플데이터가 테스트에 영향을 주지 않도록 yml파일을 설정해줍니다.

 

spring:
  profiles:
    active: local
spring:
  profiles:
    active: local
  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

 

 

테스트의 yml은 profile active를 test로 지정합니다.

spring:
  profiles:
    active: 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

 

 

 

이렇게 되면 application을 실행할때 local 이름의 yml이 동작하게 됩니다.

 

 

 

 

application이 실행될때 샘플 데이터가 들어가도록 InitData 클래스를 작성합니다.

 

@Profile("local")을 붙여주었기 때문에 위에서 설정한 local yml이 동작할때 아래 Init 코드가 동작하게 됩니다.

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

    }
}

 

 

 

Init() 분리

여기서 Init() 메서드는 @PostConstruct가 붙은 것과 @Transactional이 붙은 것이 따로 있습니다.

굳이 이렇게 분리한 이유는 spring life cycle 때문에 @PostConstruct와 @Transactional을 같이 못쓰기 때문입니다.

 

 

 

 

yml을 분리해주어 test의 yml 이름은 test 입니다.

그러므로 테스트에서 실행되면 local yml 동작하지 않고 Init Data 코드도 동작하지 않습니다.

 

 

 

 

 

조회 API 컨트롤러 개발

 

package study.querydsl.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import study.querydsl.dto.MemberSearchCondition;
import study.querydsl.dto.MemberTeamDto;
import study.querydsl.repository.MemberJpaRepository;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberJpaRepository memberJpaRepository;

    @GetMapping("/v1/members")
    public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition) {
        return memberJpaRepository.search(condition);
    }
}

[
    {
        "memberId": 3,
        "username": "member0",
        "age": 0,
        "teamId": 1,
        "teamName": "teamA"
    },
    {
        "memberId": 4,
        "username": "member1",
        "age": 1,
        "teamId": 2,
        "teamName": "teamB"
    },
    {
        "memberId": 5,
        "username": "member2",
        "age": 2,
        "teamId": 1,
        "teamName": "teamA"
    },
    {
        "memberId": 6,
        "username": "member3",
        "age": 3,
        "teamId": 2,
        "teamName": "teamB"
    },
    {
        "memberId": 7,
        "username": "member4",
        "age": 4,
        "teamId": 1,
        "teamName": "teamA"
    },
    {
        "memberId": 8,
        "username": "member5",
        "age": 5,
        "teamId": 2,
        "teamName": "teamB"
    },
    {
        "memberId": 9,
        "username": "member6",
        "age": 6,
        "teamId": 1,
        "teamName": "teamA"
    },
    {
        "memberId": 10,
        "username": "member7",
        "age": 7,
        "teamId": 2,
        "teamName": "teamB"
    },
    {
        "memberId": 11,
        "username": "member8",
        "age": 8,
        "teamId": 1,
        "teamName": "teamA"
    },
    {
        "memberId": 12,
        "username": "member9",
        "age": 9,
        "teamId": 2,
        "teamName": "teamB"
    },
    {
        "memberId": 13,
        "username": "member10",
        "age": 10,
        "teamId": 1,
        "teamName": "teamA"
    },
    {
        "memberId": 14,
        "username": "member11",
        "age": 11,
        "teamId": 2,
        "teamName": "teamB"
    },
    
    ...

 

 

 

조건을 추가해보겠습니다.

 

 

teamName

[
    {
        "memberId": 4,
        "username": "member1",
        "age": 1,
        "teamId": 2,
        "teamName": "teamB"
    },
    {
        "memberId": 6,
        "username": "member3",
        "age": 3,
        "teamId": 2,
        "teamName": "teamB"
    },
    {
        "memberId": 8,
        "username": "member5",
        "age": 5,
        "teamId": 2,
        "teamName": "teamB"
    },
    {
        "memberId": 10,
        "username": "member7",
        "age": 7,
        "teamId": 2,
        "teamName": "teamB"
    },
    {
        "memberId": 12,
        "username": "member9",
        "age": 9,
        "teamId": 2,
        "teamName": "teamB"
    },

    ...

 

 

teamName, age 

[
    {
        "memberId": 34,
        "username": "member31",
        "age": 31,
        "teamId": 2,
        "teamName": "teamB"
    },
    {
        "memberId": 36,
        "username": "member33",
        "age": 33,
        "teamId": 2,
        "teamName": "teamB"
    },
    {
        "memberId": 38,
        "username": "member35",
        "age": 35,
        "teamId": 2,
        "teamName": "teamB"
    }
]

 

 

 

 

teamName, age, username

[
    {
        "memberId": 34,
        "username": "member31",
        "age": 31,
        "teamId": 2,
        "teamName": "teamB"
    }
]

 

'Web > QueryDSL' 카테고리의 다른 글

[QueryDSL] EP5. 실무활용-스프링 데이터 JPA와 Querydsl  (0) 2021.06.17
[QueryDSL] EP3. 중급 문법  (0) 2021.06.14
[QueryDSL] EP2. 기본 문법  (0) 2021.06.10
[QueryDSL] EP1. QueryDSL 설정  (3) 2021.06.08