순수 JPA와 Querydsl Repository

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.*;

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

@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
    private final EntityManager em;
    private final JPAQueryFactory queryFactory;

    public void save(Member member){
        em.persist(member);
    }

    public Optional<Member> findById(Long id){
        return Optional.ofNullable(em.find(Member.class ,id));
    }

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

    public List<Member> findByUsername_Querydsl(String username){
        return queryFactory
                .selectFrom(member)
                .where(member.username.eq(username))
                .fetch();
    }
}

Spring Data JPA 없이 JPA와 querydsl을 사용한 예시이다.

참고로, @RequiredArgsConstructor로 JPAQueryFactory에 넣어줄 수 있는 이유는 Spring Bean으로 등록해놨기 때문이다. 아래로 예시를 들어주겠다.

@SpringBootApplication
public class QuerydslApplication {

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

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

 

 

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


MemberTeamDto 생성

package study.querydsl.dto;

import com.querydsl.core.annotations.QueryProjection;
import lombok.Data;

@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;
    }
}
Member와 Team을 함께 반환해줄 dto 생성

MemberSearchCondition 생성

package study.querydsl.dto;

import lombok.Data;

@Data
public class MemberSearchCondition {
    // 회원명, 팀명, 나이 (ageGoe, ageLoe)

    private String username;
    private String teamName;
    private Integer ageGoe;
    private Integer ageLoe;
}
파라미터 조건에따라 MemberTeamDto를 반환해줄 파라미터 안에 넣을  클래스

MemberJpaRepository 코드 추가

public class MemberJpaRepository {
    ...

    public List<MemberTeamDto> searchByBuilder(MemberSearchCondition memberSearchCondition){
        BooleanBuilder builder = new BooleanBuilder();
        if(StringUtils.hasText(memberSearchCondition.getUsername())) {
            builder.and(member.username.eq(memberSearchCondition.getUsername()));
        }
        if(StringUtils.hasText(memberSearchCondition.getTeamName())) {
            builder.and(team.name.eq(memberSearchCondition.getTeamName()));
        }
        if(memberSearchCondition.getAgeGoe() != null){
            builder.and(member.age.goe(memberSearchCondition.getAgeGoe()));
        }
        if(memberSearchCondition.getAgeLoe() != null){
            builder.and(member.age.loe(memberSearchCondition.getAgeLoe()));
        }

        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(builder)
                .fetch();
    }
}
BooleanBuilder를 사용해서 조건을 만들어주어서 where문 안에 넣는 예시이다.

참고로, StringUtils.hasText()는 null이나 ""빈 값 모두 false로 걸러주는 기능을 해준다.

 

음.. 확실히 이렇게 보니 가독성이 떨어지는 것 같긴하다..

그렇다면, where절은 조금 더 깔끔하겠지?!

 

 

 

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


MemberJpaRepository 코드 추가

public class 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 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 (ageGoe == null)? null : member.age.goe(ageGoe);
    }

    private BooleanExpression ageLoe(Integer ageLoe) {
        return (ageLoe == null)? null : member.age.loe(ageLoe);
    }
}

이렇게 각각 기능들을 메소드로 분리함으로써 가독성도 높아지고, 재사용성도 생긴다. 개인적으로 BooleanBuilder보다 where절이 객체지향 프로그래밍에 맞는 코드인 것 같다.

 

 

 

조회 API 컨트롤러 개발


사전 준비

main에 있는 application.yml을 test에도 추가해줍니다. (resources 파일도 추가해야 함)

그리고 설정을 아래와 같이 local부분만 다르게 test로 작성해줍니다. (main도 profiles: active: local 추가)

spring:
  profiles:
    active: local			// main (test 경우에는 local이 아닌 test)
  datasource:
    url: jdbc:h2:tcp://localhost/~/querydsl
...

그 후 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 selectTeam = i % 2 == 0? teamA : teamB;
                em.persist(new Member("member"+i, i, selectTeam));
            }
        }
    }
}

InitMemberService를 따로 만들어준 이유는 @PostConstruct와 @Transactional을 함께 사용하지 못하기 때문에 분리하기 위해 만들어 주었다. @Profiles("local")은 이전에 application.yml에 설정해주었던 profiles 값이 Local인 경우에만 구동시켜준다.

 

사전 준비는 끝났다. 게임을 시작해보자.

 

MemberController 생성

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> seachMemberV1(MemberSearchCondition condition) {
        return memberJpaRepository.search(condition);
    }

}

만들고 주소/v1/members로 검색하면 이전에 넣어주었던 데이터들이 나온다. (웹에서 접근하면 보기 힘들기 때문에 postman과 같은 api툴을 사용하기를 바란다!)

위와같이 파라미터 조건을 추가하면 조건에 맞게 조회된 데이터를 확인할 수 있다.

참고로 아무런 파라미터를 설정하지 않는다면 모든데이터가 조회되기 때문에, 사용할 때 각별히 주의하자!!!!

 

 

 

 

 

출처

 

실전! Querydsl - 인프런 | 강의

Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!, - 강의 소개 | 인프런...

www.inflearn.com

 

+ Recent posts