반응형
본 포스트는 인프런 - 실전! QueryDSL의 강의를 듣고 공부한 내용입니다
https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84
1. 순수 JPA와 QueryDSL 예제
- 순수 JPA Repository 예제
// 순수 JPA @Repository public class MemberJpaRepository { private final EntityManager em; private final JPAQueryFactory jpaQueryFactory; public MemberJpaRepository(EntityManager em) { this.em = em; jpaQueryFactory = new JPAQueryFactory(em); } public void save(Member member) { em.persist(member); } public Optional<Member> findById(final Long id) { return Optional.ofNullable(em.find(Member.class, id)); } public List<Member> findAll() { return em.createQuery("SELECT m FROM Member m", Member.class) .getResultList(); } public List<Member> findByUsername(final String username) { return em.createQuery("SELECT m FROM Member m WHERE m.userName = :username", Member.class) .setParameter("username", username) .getResultList(); } }
- 작성이 잘되었는지 테스트 코드 작성
// Test @ActiveProfiles("dev") @SpringBootTest @Transactional class MemberJpaRepositoryTest { @Autowired private EntityManager em; @Autowired private MemberJpaRepository memberJpaRepository; @Test void basicTest() { Member member = new Member("member1", 10); memberJpaRepository.save(member); Member findMember = memberJpaRepository.findById(member.getId()).get(); assertThat(findMember).isEqualTo(member); List<Member> result1 = memberJpaRepository.findAll(); assertThat(result1).containsExactly(member); List<Member> result2 = memberJpaRepository.findByUsername("member1"); assertThat(result2).containsExactly(member); } }
- ActiveProfile("dev")는 강의와 다르게 MySQL 환경에서 실행시키기 위해서 profile을 나눠놨기 때문에 붙였습니다 ( 강의는 H2 database 이용 )
- 기존 문자열로 JPQL을 정의했던 MemberJpaRepository::findAll() 메서드를 QueryDSL로 재정의
// QueryDSL public List<Member> findAllUsingQueryDSL() { return jpaQueryFactory .selectFrom(member) .fetch(); }
- 여기서 member는 QMember.member에서 static import를 활용
- findByUsername 재정의
// QueryDSL public List<Member> findByUsernameUsingQueryDSL(final String username) { return jpaQueryFactory .selectFrom(member) .where(member.userName.eq(username)) .fetch(); }
- QueryDSL 장점
- 기존의 JPQL(문자열)로 정의한 메서드는 런타임 시점에서 오류를 잡을 수 있지만, QueryDSL은 메서드 체이닝 방식으로 정의하기 때문에 컴파일 시점에서 오류를 잡을 수 있다
- 코드 간결성
- 심플한 파라미터 바인딩
- 추가 팁
- JPAQueryFactory는 멀티쓰레딩 환경에서 동작 가능하기 때문에 SpringBean으로 미리 등록해놓고 DI 받아도된다
2. 동적 쿼리와 성능 최적화 조회
- MemberTeamDto 생성
// MemberTeamDto.class @Data public class MemberTeamDto { private Long memberId; private String userName; private int age; private Long teamId; private String teamName; @QueryProjection 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; } }
- @QueryProjection을 이용해 compile된 Q-Type 클래스를 이용 ( queryDSL 컴파일 필요 )
- 검색 조건을 위한 DTO 생성
// 검색조건 @Data public class MemberSearchCondition { private String userName; private String teamName; private Integer ageGoe; private Integer ageLoe; }
2-1. BooleanBuilder를 이용한 동적 쿼리
- 예제 코드
// Repository public List<MemberTeamDto> searchByBuilder(final MemberSearchCondition condition) { BooleanBuilder builder = new BooleanBuilder(); 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 (Objects.nonNull(condition.getAgeGoe())) { builder.and(member.age.goe(condition.getAgeGoe())); } if (Objects.nonNull(condition.getAgeLoe())) { builder.and(member.age.loe(condition.getAgeLoe())); } return jpaQueryFactory .select(new QMemberTeamDto( member.id.as("memberId"), member.userName, member.age, team.id.as("teamId"), team.name )) .from(member) .leftJoin(member.team, team) .where(builder) .fetch(); }
- 테스트 코드
// test @ActiveProfiles("dev") @SpringBootTest @Transactional public class SearchTest { @Autowired private EntityManager em; @Autowired private MemberJpaRepository memberJpaRepository; @BeforeEach void setUp() { // given Team teamA = new Team("teamA"); Team teamB = new Team("teamB"); // when em.persist(teamA); em.persist(teamB); // given Member memberA = new Member("memberA", 10, teamA); Member memberB = new Member("memberB", 20, teamA); Member memberC = new Member("memberC", 30, teamB); Member memberD = new Member("memberD", 40, teamB); // when em.persist(memberA); em.persist(memberB); em.persist(memberC); em.persist(memberD); } @Test void searchTest() { // given MemberSearchCondition condition = new MemberSearchCondition(); condition.setAgeGoe(35); condition.setAgeLoe(40); condition.setTeamName("teamB"); // when List<MemberTeamDto> actual = memberJpaRepository.searchByBuilder(condition); // then assertThat(actual) .hasSize(1) .extracting("userName") .containsExactly("memberD"); } }
- 실행 쿼리
# JPQL /* select member1.id as memberId, member1.userName, member1.age, team.id as teamId, team.name from Member member1 left join member1.team as team where team.name = ?1 and member1.age >= ?2 and member1.age <= ?3 */ # SQL Query select member0_.member_id as col_0_0_, member0_.user_name as col_1_0_, member0_.age as col_2_0_, team1_.id as col_3_0_, team1_.name as col_4_0_ from member member0_ left outer join team team1_ on member0_.team_id=team1_.id where team1_.name=? and member0_.age>=? and member0_.age<=?
- 동적 쿼리 주의사항
- 위 예제에서 BooleanBuilder에 조건이 전부 빠지면, 전체 테이블을 조회하게 된다 ( 성능 이슈가 발생할 수 있음 )
- 최소 조건을 정의하거나 안되면 limit라도 주는 것이 안전하다 ( 가급적 페이징 쿼리가 같이 들어가주는게 좋음 )
2-2. Where절 파라미터 사용
- 위에서 BooleanBuilder를 이용하여 동적쿼리 생성한 메서드를 Where 파라미터로 변경해보겠습니다
- 예제 코드
// Repository public List<MemberTeamDto> search(final MemberSearchCondition condition) { return jpaQueryFactory .select(new QMemberTeamDto( member.id.as("memberId"), member.userName, member.age, team.id.as("teamId"), 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 Objects.nonNull(ageGoe)? member.age.goe(ageGoe) : null; } private BooleanExpression ageLoe(Integer ageLoe) { return Objects.nonNull(ageLoe)? member.age.loe(ageLoe) : null; }
- Where절 파라미터로 넘기는 동적쿼리의 장점
- 가독성면에서 BooleanBuilder보다 좋다
- 만들어둔 조건 메서드들이 재사용이 가능하다
- npt(nullPointException)만 조심한다면 각 조건 메서드도 조합이 가능하다 ( BooleanExpression로 반환하는 이유 )
728x90
반응형
'Study > 실전! QueryDSL' 카테고리의 다른 글
Day 6. Spring Data JPA에서 지원하는 QueryDSL (0) | 2021.10.07 |
---|---|
Day 5. Spring Data JPA와 QueryDSL (0) | 2021.10.06 |
Day 3. QueryDSL 중급 문법 (0) | 2021.10.03 |
Day 2. QueryDSL 기본 문법 (0) | 2021.09.24 |
Day 1. QueryDSL 소개 및 프로젝트 설정 (0) | 2021.09.19 |