query dsl - 2

김강현·2023년 5월 5일
0

query-dsl

목록 보기
3/3

실무 활용

Repository 연동

<MemberJpaRepository.java>

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

<MemberTeamDto.java>

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

<MemberSearchCondition.java>

@Data
public class MemberSearchCondition {
    private String username;
    private String teamName;
    private Integer ageGoe;
    private Integer ageLoe;
}

ctrl + shift + enter 구문 자동 완성!!

BooleanBuilder 동적 쿼리

    public List<MemberTeamDto> searchByBuilder(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(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("memberId"),
                        member.username,
                        member.age,
                        team.id.as("teamId"),
                        team.name.as("teamName")
                ))
                .from(member)
                .leftJoin(member.team, team)
                .where(builder)
                .fetch();
    }

where 동적 쿼리

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

    // BooleanExpression 이 추후  composition 에 유리
    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 num) {
        return num != null ? member.age.goe(num) : null;
    }

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

where 절... 너무 깔끔하잖아!! 좋아좋아 최고!
재사용 및 조립도 가능함!

API 컨트롤러

샘플 데이터 설정

  • 테스트를 돌릴때는, 샘플 데이터 추가 로직이 동작 x
  • 톰캣을 띄울때는, 샘플 데이터 추가 로직이 실행 되는 걸로!

application.yml 에 profiles -> active: local 추가

test 폴더에 application.yml 추가

다 똑같은데, profiles active 를 test 로 설정!

<InitMember.java> 파일 생성

@Profile("local")
@ComponentScan
@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") 은 profile local 에서만 실행하겠다는 어노테이션
@ComponentScan 은 spring 이 돌아갈떄, component scan 을 하라는 뜻.
@PostConstruct 는 시작할때 실행을 하겠다는 뜻
@Component 는 spring bean 에 component 로 지정

  • 굳이 InitMemberService 를 만들어서 init 해주는 이유는 JPA 기능인 @Transactional@PostConstruct 랑 함께 지정할 수가 없기 때문!!

api controller

<MemberController.java>

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

호출하는 법

스프링 데이터 JPA Repository 로 변경

기존 <MemberJpaRepository.java>

Spring 데이터 JPA 를 활용한 <MemberRepository.java>

Query dsl 은 사용자 정의 리포지토리를 통해 구현한다.

사용자 정의 리포지토리

스프링 데이터 JPA 에서 했던 방식.

<MemberRepositoryCumstom.java> 파일 생성

public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(MemberSearchCondition condition);
}

<MemberRepositoryImpl.java> 파일 생성

public class MemberRepositoryImpl implements MemberRepositoryCustom{

    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())
                )
                .fetch();
    }
    ...
    // usernameEq, teamNameEq, ageGoe, ageLoe 함수
}

<MemberRepository.java> 파일

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
    List<Member> findByUsername(String username);
}

MemberRepositoryCustom 이라는 interface를 만들어, MemberRepositoryImpl 로 구현

  • 이때 구현체는 JpaRepository를 상속받는 리포지토리 뒤에 impl 붙은 이름과 같아야함
    (MemberRepository, MemberRepositoryImpl)
  • MemberRepositoryJpaRepository<해당 인터페이스> 를 상속받으면 됨.

조회 쿼리가 너무 복잡한 경우. MemberRepository 에 합치는 것이 아니라,
새로운 MemberQueryRepository 를 만드는 것도 좋음.
특화된 기능은 따로 만드는 것도 좋음!! 합치는 것이 무조건 정답이 아님.

  • 비즈니스, 설계에 따라 유동적으로 도입할 줄 아는 유연성을 가질 것!!

스프링 데이터 페이징 활용

  • Page, Pageable 을 활용해보기!
  • simple 과 complex 두 버전
public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(MemberSearchCondition condition);
    /*추가*/ Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
    /*추가*/ Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}

simple pageable

기존 쿼리문에 offsetlimit 를 추가 해주는 방식으로 pageable 구현이 가능하다!!
PageImpl 구현체를 사용하여 Page 타입으로 반환 가능!

테스트 코드

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

        Member m1 = new Member("m1", 10, teamA);
        Member m2 = new Member("m2", 20, teamA);

        Member m3 = new Member("m3", 15, teamB);
        Member m4 = new Member("m4", 25, teamB);

        Member m5 = new Member("m5", 30);
        Member m6 = new Member("m6", 40);

        em.persist(m1);
        em.persist(m2);
        em.persist(m3);
        em.persist(m4);
        em.persist(m5);
        em.persist(m6);

        em.flush();
        em.clear();

        // when
        MemberSearchCondition condition = new MemberSearchCondition();
        PageRequest pageRequest = PageRequest.of(0, 3);

        Page<MemberTeamDto> result = memberRepository.searchPageSimple(condition, pageRequest);

        // then

        assertThat(result.getSize()).isEqualTo(3);
        assertThat(result.getContent()).extracting("username").containsExactly("m1", "m2", "m3");

    }

이 방법의 경우, getTotal() 을 통해 count 를 불러오는데, 이때 count query에 특정 쿼리를 지정해주면 좋다.

complex pageable

result 를 그냥 fetch 를 활용하여 그대로 받아오고,
fetchCount 를 활용해서, 최적화된 count 함수를 실행함.

핵심은 count 를 직접 지정해주어서, count와 상관 없는 쿼리가 포함되는 것을 막을 수 있음 (성능 최적화)
count 가 0일때는 search 쿼리를 날리지 않는 다던지 하는
데이터가 얼마 없을때는 그냥... fetchResult 써라...

count 쿼리 최적화

  • 상황에 따라서는 count 쿼리를 생략할 수 있다.
  • 1) 페이지 시작이면서, 컨텐츠 사이즈가 페이지 사이즈 보다 작을때
  • 2) 마지막 페이지 일때
        JPAQuery<Member> countQuery = queryFactory
                .selectFrom(member)
                .leftJoin(member.team, team)
                .where(
                        usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                );

        return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount());

스프링데이터 JPA 에서 PageableExecutionUtils 로 그 기능을 제공해준다.
세번째 input 으로는 page count 가 필요할때 실행할 함수를 넣어주면 된다!

if 문 넣어서 직접 해도 되긴함!!


이런식으로 controller 에서 불러와 사용하면 됨!

스프링 데이터 JPA 가 지원하는 Querydsl

여러개 가 있기는 하지만, 복잡한 실무에서 사용하기는 좀 애매함... 참고만!!

  • QuerydslPredicateExecutor
    1 - query dsl 코드를 인자로 넘길 수 있음
    2 - left join 지원 안해줌.
    3 - 서비스, 컨트롤러 계층에 query 문이 들어감

  • Querydsl Web 지원
    1 - @QuerydslPredicate 를 활용해서 파라미터를 편하게 받을 수 있음
    2 - 조건문이 한정적임 / custom 하려면 너무 복잡해짐
    3 - 마찬가지로 상위 계층에 query 관련 function 이 포함됨.

  • QuerydslRepositorySupport
    1 - 추상클래스임 (특정 리포지토리에 상속시키면 됨)
    2 - queryFactory 없이, 구현된 특정 메소드들을 활용하여 쉽게 메소드 짤 수 있음

Querydsl 지원 클래스 직접 만들기 (Advance)

김영한 강사님의. QuerydslRepositorySupport 한계 극복 클래스 직접 제작!!

@Repository
public abstract class Querydsl4RepositorySupport {
    private final Class domainClass;
    private Querydsl querydsl;
    private EntityManager entityManager;
    private JPAQueryFactory queryFactory;
    public Querydsl4RepositorySupport(Class<?> domainClass) {
        Assert.notNull(domainClass, "Domain class must not be null!");
        this.domainClass = domainClass;
    }
    @Autowired
    public void setEntityManager(EntityManager entityManager) {
        Assert.notNull(entityManager, "EntityManager must not be null!");
        JpaEntityInformation entityInformation =
                JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager);
        SimpleEntityPathResolver resolver = SimpleEntityPathResolver.INSTANCE;
        EntityPath path = resolver.createPath(entityInformation.getJavaType());
        this.entityManager = entityManager;
        this.querydsl = new Querydsl(entityManager, new
                PathBuilder<>(path.getType(), path.getMetadata()));
        this.queryFactory = new JPAQueryFactory(entityManager);
    }
    @PostConstruct
    public void validate() {
        Assert.notNull(entityManager, "EntityManager must not be null!");
        Assert.notNull(querydsl, "Querydsl must not be null!");
        Assert.notNull(queryFactory, "QueryFactory must not be null!");
    }
    protected JPAQueryFactory getQueryFactory() {
        return queryFactory;
    }
    protected Querydsl getQuerydsl() {
        return querydsl;
    }
    protected EntityManager getEntityManager() {
        return entityManager;
    }
    protected <T> JPAQuery<T> select(Expression<T> expr) {
        return getQueryFactory().select(expr);
    }
    protected <T> JPAQuery<T> selectFrom(EntityPath<T> from) {
        return getQueryFactory().selectFrom(from);
    }
    protected <T> Page<T> applyPagination(Pageable pageable,
                                          Function<JPAQueryFactory, JPAQuery> contentQuery) {
        JPAQuery jpaQuery = contentQuery.apply(getQueryFactory());
        List<T> content = getQuerydsl().applyPagination(pageable,
                jpaQuery).fetch();
        return PageableExecutionUtils.getPage(content, pageable,
                jpaQuery::fetchCount);
    }
    protected <T> Page<T> applyPagination(Pageable pageable,
                                          Function<JPAQueryFactory, JPAQuery> contentQuery, Function<JPAQueryFactory,
            JPAQuery> countQuery) {
        JPAQuery jpaContentQuery = contentQuery.apply(getQueryFactory());
        List<T> content = getQuerydsl().applyPagination(pageable,
                jpaContentQuery).fetch();
        JPAQuery countResult = countQuery.apply(getQueryFactory());
        return PageableExecutionUtils.getPage(content, pageable,
                countResult::fetchCount);
    }
}

이런 식으로 기존에 있던게 불편하다면, 중간에 함수들을 약간 변경해서 내 맛대로 사용해도 되는 거였음!!

profile
this too shall pass

0개의 댓글