해당 포스트에서 소개하는 기능은 제약이 커서 복잡한 실무 환경에서 사용하기에는 많이 부족하다고 한다. 그래도 Spring Data가 제공하는 기능이므로 간단히 알아보고, 왜 부족한지 이해해보자! (Spring에서 나중에 더 좋게 버전 업그레이드 할수도 있지않을까??)

 

 

인터페이스 지원 - QuerydslPredicateExecutor


공식 문서

 

Spring Data JPA - Reference Documentation

Example 108. Using @Transactional at query methods @Transactional(readOnly = true) public interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") v

docs.spring.io

최근 문서

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

사용 방법은 JPARepository에 상속하면 된다.

MemberRepository 코드 추가

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom, QuerydslPredicateExecutor<Member> {
}

테스트

    @Test
    public void querydslPredicateExcutorTest() {
        Iterable<Member> result = memberRepository.findAll(QMember.member.age.between(20, 40).and(QMember.member.username.eq("member3")));

        for (Member member : result) {
            System.out.println("member = " + member);
        }
    }

findAll()로 모든 Member를 조회하는데, 조건을 추가할 수 있다. age가 20~30이며, username이 member3인 Member를 조회했다.

현재까지는 굉장히 좋은 것 같다. 뭐가 문제일까??

 

한계점

  • 조인 X (RDB인데 조인이 불가능하다고?!) - 묵시적 조인은 가능하지만, left join이 불가능하다고 한다.
  • 클라이언트가 Querydsl에 의존해야 한다. 서비스 클래스가 Querydsl이라는 구현 기술에 의존해야 한다.
    • 이렇게하면 Repository를 만드는 의미가 없다.
    • Repository를 만드는 이유는 복잡한 쿼리같은 경우 Querydsl을 repository계층 내부에 사용해서 외부로는 노출이 되지 않기 때문에 코드를 변경할 때 Repository 계층만 수정하면 된다.
    • 하지만, Querydsl이 Service 계층으로 노출되는 경우 Service 계층도 수정을 해야한다.
  • 복잡한 실무환경에서 사용하기에는 한계가 명확하다.
참고
QuerydslPredicateExecutor는 Pageable, Sort를 모두 지원하고 정상 동작한다.

 

 

 

Querydsl web 지원


공식 문서

 

Spring Data JPA - Reference Documentation

Example 108. Using @Transactional at query methods @Transactional(readOnly = true) public interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") v

docs.spring.io

최근 문서

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

 

한계점

  • 단순한 조건만 가능 (eq 정도만 가능)
  • 조건을 커스텀하는 기능이 복잡하고 명시적이지 않음
  • 컨트롤러가 Querydsl에 의존
  • 복잡한 실무환경에서 사용하기에는 한계가 명확 

이건 쓰지말자...

 

 

 

Repository 지원 - QuerydslRepositorySupport


장점

  • getQuerydsl().applyPagination()
    • 스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게 변환 가능 (단! Sort는 오류 발생) ??
  • from() 으로 시작가능 (최근 QueryFactory를 사용해서 select()로 시작하는 것이 더 명시적이다)
  • EntityManager 제공 (주입받지 않아도 된다.)

뭐 지금까지 나쁘지 않아 보인다. 코드를 몇줄 줄일 수 있을법하다. 하지만 한계가 명확하게 존재하다고 한다.

 

한계

  • Querydsl 3.x 버전을 대상으로 만듬
  • Querydsl 4.x 에 나온 JPAQueryFactory로 시작할 수 없음
    • select로 시작할 수 없음 (from 시작)
  • QueryFactory를 제공하지 않음
  • 스프링 데이터 Sort 기능이 정상 동작하지 않음

 

 

 

Querydsl 지원 클래스 직접 만들기


스프링 데이터가 제공하는 QuerydslRepositorySupport가 지닌 한계를 극복하기 위해 직접 Querydsl 지원 클래스를 만들어보자!! 

 

장점

  • 스프링 데이터가 제공하는 페이징을 편리하기 변환
  • 페이징과 카운트 쿼리 분리 가능
  • 스프링 데이터 Sort 지원
  • select(), selectFrom() 으로 시작 가능
  • EntityManager, QueryFactory 제공

 

Querydsl4RepositorySupport

package study.querydsl.repository.support;

import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;
import org.springframework.data.jpa.repository.support.Querydsl;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
import org.springframework.data.repository.support.PageableExecutionUtils;
import org.springframework.stereotype.Repository;
import org.springframework.util.Assert;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.function.Function;
/**
 * Querydsl 4.x 버전에 맞춘 Querydsl 지원 라이브러리
 *
 * @author Younghan Kim
 * @see
org.springframework.data.jpa.repository.support.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);
    }
}

MemberTestRepository

package study.querydsl.repository;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQuery;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.support.PageableExecutionUtils;
import org.springframework.stereotype.Repository;
import study.querydsl.dto.MemberSearchCondition;
import study.querydsl.entity.Member;
import study.querydsl.repository.support.Querydsl4RepositorySupport;
import java.util.List;
import static org.springframework.util.StringUtils.isEmpty;
import static study.querydsl.entity.QMember.member;
import static study.querydsl.entity.QTeam.team;
@Repository
public class MemberTestRepository extends Querydsl4RepositorySupport {
    public MemberTestRepository() {
        super(Member.class);
    }
    public List<Member> basicSelect() {
        return select(member)
                .from(member)
                .fetch();
    }
    public List<Member> basicSelectFrom() {
        return selectFrom(member)
                .fetch();
    }
    public Page<Member> searchPageByApplyPage(MemberSearchCondition condition,
                                              Pageable pageable) {
        JPAQuery<Member> query = selectFrom(member)
                .leftJoin(member.team, team)
                .where(usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()));
        List<Member> content = getQuerydsl().applyPagination(pageable, query)
                .fetch();
        return PageableExecutionUtils.getPage(content, pageable,
                query::fetchCount);
    }

    public Page<Member> applyPagination(MemberSearchCondition condition,
                                        Pageable pageable) {
        return applyPagination(pageable, contentQuery -> contentQuery
                .selectFrom(member)
                .leftJoin(member.team, team)
                .where(usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())));
    }

    public Page<Member> applyPagination2(MemberSearchCondition condition,
                                         Pageable pageable) {
        return applyPagination(pageable, contentQuery -> contentQuery
                        .selectFrom(member)
                        .leftJoin(member.team, team)
                        .where(usernameEq(condition.getUsername()),
                                teamNameEq(condition.getTeamName()),
                                ageGoe(condition.getAgeGoe()),
                                ageLoe(condition.getAgeLoe())),
                countQuery -> countQuery
                        .selectFrom(member)
                        .leftJoin(member.team, team)
                        .where(usernameEq(condition.getUsername()),
                                teamNameEq(condition.getTeamName()),
                                ageGoe(condition.getAgeGoe()),
                                ageLoe(condition.getAgeLoe()))
        ); }
    private BooleanExpression usernameEq(String username) {
        return isEmpty(username) ? null : member.username.eq(username);
    }
    private BooleanExpression teamNameEq(String teamName) {
        return isEmpty(teamName) ? null : team.name.eq(teamName);
    }
    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);
    }
}

 

 

 

 

 

 

출처

 

실전! Querydsl - 인프런 | 강의

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

www.inflearn.com

 

Spring Data JPA 리포지토리 변경


package study.querydsl.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import study.querydsl.entity.Member;
import java.util.*;

public interface MemberRepository extends JpaRepository<Member, Long> {
    // 메소드 쿼리 복습
    // select m from Member m where m.username = username
    List<Member> findByUsername(String username);

}

interface로 만들어야하는 것 잊지말자!!

 

 

사용자 정의 리포지토리


이전에 Spring Data JPA를 공부하며 나왔던 개념이다.  Spring Data JPA에서 제공하는 기능 외 동적 쿼리등 복잡한 쿼리를 만들고 싶을 때 사용해야되는 방법이다. 사용자 정의 리포지토리를 만드는 방법은 아래와 같다.

 

사용자 정의 리포지토리 사용법

  1. 사용자 정의 인터페이스 작성
  2. 사용자 정의 인터페이스 구현
  3. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속

그림으로 보면 위와 같다. JpaRepository를 상속받은 MemberRepository가 있다. 동적 쿼리등 복잡한 쿼리를 추가하고 싶은데, Spring Data JPA에서 제공하는 기능으로는 한계가있다.

1. MemberRepositoryCustom interface 생성

2. MemberRepositoryImpl 생성 후 기능 구현

3. MemberRepository에 MemberRepositoryCustom 상속

여기서 지켜야하는 룰이 하나있다.

추가 기능을 수행하는 클래스 명은 = "JPARepository를 상속받은 클래스 명" + "아무값(없어도 됌)" + "Impl" 로 작성해야 한다.

 

MemberRepositoryImpl에는 기존에 qeurydsl로 만들었던 search 메소드와 연관된 메소드를 그대로 넣어주었다.

 

 

 

스프링 데이터 페이징 활용


  • 스프링 데이터의 Page, Pageable을 활용해보자.
  • 전체 카운트를 한번에 조회하는 단순한 방법
  • 데이터 내용과 전체 카운트를 별도로 조회하는 방법

전체 카운트를 한번에 조회하는 단순한 방법

    @Override
    public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
        QueryResults<MemberTeamDto> results = 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()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();

        List<MemberTeamDto> content = results.getResults();
        long total = results.getTotal();

        return new PageImpl<>(content, pageable, total);
    }

데이터 내용과 전체 카운트를 별도로 조회하는 방법

    @Override
    public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = 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()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        long total = queryFactory
                .selectFrom(member)
                .leftJoin(member.team, team)        // 어떤 상황에는 join이 필요없는 경우가 있다.
                .where(
                        usernameEq(condition.getUsername()),
                        teamnameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .fetchCount();
        return new PageImpl<>(content, pageable, total);
    }

별도로 조회하는 방법은 성능을 조금 높이기 위함일 수 있습니다. 예를들어, count 쿼리를 조회한 뒤 count가 0인 경우 content 쿼리를 하지 않을 수 있습니다. 아니면 카운트 쿼리를 content 쿼리보다 단순하게 조회할 수 있는 경우 성능상 이점이 생길 수 있습니다.

참고
Querydsl이 향후 fetchCount()를 제공하지 않습니다. 
fetchCount() => fetchOne()
fetchresults() => fetch() 권장

 

 

CountQuery 최적화


count 쿼리를 생략할 수 있는 경우는 아래와 같다.

  1. 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때 
  2. 마지막 페이지 일 때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함)

위의 최적화를 할 수 있는 코드는 count쿼리를 따로 사용할 때 사용할 수 있다.

    @Override
    public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = 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()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        // countQuery에서는 fetchCount()를 해야 카운트 쿼리가 날라간다.
        JPAQuery<Member> countQuery = queryFactory
                .selectFrom(member)
                .leftJoin(member.team, team)        // 어떤 상황에는 join이 필요없는 경우가 있다.
                .where(
                        usernameEq(condition.getUsername()),
                        teamnameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe())
                );

        // PageableExecutionUtils.getPage에서는 content pagealbe의 토탈 사이즈를 보고 count 쿼리를 생략할 수 있는 경우에
        // fetchCount() 쿼리 함수를 실행하지 않는다.
        return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount());
        //return new PageImpl<>(content, pageable, total);
    }
참고
Querydsl이 향후 fetchCount()를 제공하지 않습니다. 
fetchCount() => fetchOne()
PagealbeExecutionUtils.getPage에서는 content, pageable의 토탈 사이즈를 보고 count 쿼리를 생략할 수 있는 경우에 fetchCount()를 실행하지 않기 때문에 count쿼리가 필요하지 않는 경우 쿼리가 발생하지않는다.

getPage코드는 아래와 같다.

	public static <T> Page<T> getPage(List<T> content, Pageable pageable, LongSupplier totalSupplier) {

		Assert.notNull(content, "Content must not be null!");
		Assert.notNull(pageable, "Pageable must not be null!");
		Assert.notNull(totalSupplier, "TotalSupplier must not be null!");

		if (pageable.isUnpaged() || pageable.getOffset() == 0) {

			if (pageable.isUnpaged() || pageable.getPageSize() > content.size()) {
				return new PageImpl<>(content, pageable, content.size());
			}

			return new PageImpl<>(content, pageable, totalSupplier.getAsLong());
		}

		if (content.size() != 0 && pageable.getPageSize() > content.size()) {
			return new PageImpl<>(content, pageable, pageable.getOffset() + content.size());
		}

		return new PageImpl<>(content, pageable, totalSupplier.getAsLong());
	}

 

 

 

Controller 개발


이제 controller를 개발해보자.

MemberController 코드 추가

public class MemberController {
    ...

    @GetMapping("/v2/members")
    public Page<MemberTeamDto> seachMemberV2(MemberSearchCondition condition, Pageable pageable) {
        return memberRepository.searchPageSimple(condition, pageable);
    }

    @GetMapping("/v3/members")
    public Page<MemberTeamDto> seachMemberV3(MemberSearchCondition condition, Pageable pageable) {
        return memberRepository.searchPageComplex(condition, pageable);
    }
}

이제 postman에서 아래와 같이 page, size를 바꿔가며 데이터를 조회해보면 페이지별로 조회가 가능하다.

localhost:8080/v2/members?page=0&size=5

테스트를 해보니 V3의 경우 데이터 < 조회한 데이터 크기 경우 count 쿼리가 나가지 않는 것을 확인할 수 있습니다.

 

 

 

스프링 데이터 정렬


Spring Data JPA는 자신의 정렬(Sort)를 Querydsl의 정렬(OrderSpecifier)로 편리하게 변경하는 기능을 제공한다. 해당 부분은 뒤에 Spring Data JPA가 제공하는 Querydsl 기능에서 살펴본다. 

Spring Data Sort를 Querydsl의 OrderSpecifier로 변환

    JPAQuery<Member> query = queryFactory
            .selectFrom(member);
            
    for (Sort.Order o : pageable.getSort()) {
        PathBuilder pathBuilder = new PathBuilder(member.getType(),
    member.getMetadata());
    
        query.orderBy(new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC,
                pathBuilder.get(o.getProperty())));
    }
    
    List<Member> result = query.fetch();
참고
정렬은 조건이 조금만 복잡해져도 Pagealbe의 sort기능을 사용하기 어렵다고 한다. 루트 엔티티 범위를 넘어가는 동적 정렬 기능이 필요하면 스프링 데이터 페이징이 제공하는 sort를 사용하기 보다는 파라미터를 받아서 직접 처리하는 것을 권장.

 

 

 

출처

 

실전! Querydsl - 인프런 | 강의

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

www.inflearn.com

 

순수 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

 

프로젝션과 결과 반환 - DTO 조회


    @Test
    public void findDtoByJPQL() {
        List<MemberDto> result = em.createQuery("select new study.querydsl.dto.MemberDto(m.username, m.age) from Member m", MemberDto.class)
                .getResultList();

        for (MemberDto memberDto : result) {
            System.out.println("MemberDTO = " + memberDto);
        }
    }

이전에 사용한 바 있는 JPQL을 사용해 DTO로 변환해 조회하는 코드이다. 해당 코드는 new Operation을 사용해야하기 때문에 DTO의 package 명을 다 적어줘야 한다. 이때문에 코드가 지저분해지고, 코드 작성에도 불편함이 있다. 또한, 생성자 방식만 지원하는 불편함도 있다.

 

하지만, querydsl은 위의 문제를 깔끔하게 해결하는 방법을 제시한다.

 

Querydsl 빈 생성(Bean population)

결과를 DTO 반환할 때 사용한다. 아래와 같은 3가지 방법을 지원한다.

  • 프로퍼티 접근
  • 필드 직접 접근
  • 생성자 사용

프로퍼티 접근 - Setter

    @Test
    public void findDtoBySetter() {
        List<MemberDto> result = queryFactory
                .select(Projections.bean(MemberDto.class,
                        member.username,
                        member.age))
                .from(member)
                .fetch();

        for (MemberDto memberDto : result) {
            System.out.println("MemberDto = " + memberDto);
        }
    }

 

필드 직접 접근 

    @Test
    public void findDtoByField() {
        List<MemberDto> result = queryFactory
                .select(Projections.fields(MemberDto.class,     // getter, setter 상관없이 값을 넣는다.
                        member.username,
                        member.age))
                .from(member)
                .fetch();

        for (MemberDto memberDto : result) {
            System.out.println("MemberDto = " + memberDto);
        }
    }

 

생성자 접근

    @Test
    public void findDtoByConstructor() {
        List<MemberDto> result = queryFactory
                .select(Projections.constructor(MemberDto.class,     // 파라미터 생성자에 값이 딱딱 들어간다.
                        member.username,
                        member.age))
                .from(member)
                .fetch();

        for (MemberDto memberDto : result) {
            System.out.println("MemberDto = " + memberDto);
        }
    }

 

필드 값이 다른 dto 접근 - 프로퍼티 접근법만 예시 (필드, 생성자 접근 동일)

    @Test
    public void findUserDtoBySetter() {
        List<UserDto> result = queryFactory
                .select(Projections.bean(UserDto.class,       //setter를 통해 들어간다.
                        member.username.as("name"),
                        member.age))
                .from(member)
                .fetch();

        for (UserDto memberDto : result) {
            System.out.println("MemberDto = " + memberDto);
        }
    }

.as("바꾸고싶은 필드명")으로 필드명을 바꿔주면 된다.

 

서브쿼리 추가 dto 접근 - 필드 접근법만 예시 (프로퍼티, 생성자 접근 동일)

    @Test
    public void findUserDtoByFieldSubQuery() {
        QMember memberSub = new QMember("memberSub");
        List<UserDto> result = queryFactory
                .select(Projections.fields(UserDto.class,     // getter, setter 상관없이 값을 넣는다.
                        member.username.as("name"),
                        Expressions.as(JPAExpressions
                                .select(memberSub.age.max())
                                .from(memberSub), "age")
                ))
                .from(member)
                .fetch();

        for (UserDto userDto : result) {
            System.out.println("userDto = " + userDto);
        }
    }
  • ExpressionUtils.as(source, alias) : 필드나 서브쿼리에 별칭 적용

 

 

 

프로젝션과 결과 반환 - @QueryProjection


@QueryProjection을 사용하기 위해서는 준비 작업이 필요합니다.

@Data
@NoArgsConstructor
public class MemberDto {
    private String username;
    private int age;

    @QueryProjection
    public MemberDto(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

사용하고자하는 dto 클래스의 생성자에 @QueryProjection을 추가해줍니다. 실행을 한번 해보거나 이전에 돌렸던 compilequerydsl을 눌러줍니다. 그렇게되면 @MemberDto가 생성됩니다. 이제 사용해봅시다.

 

생성자 + @QueryProjection

    @Test
    public void findDtoByQueryProjection() {
        List<MemberDto> result = queryFactory
                .select(new QMemberDto(member.username, member.age))
                .from(member)
                .fetch();

        for (MemberDto memberDto : result) {
            System.out.println("memberDto = " + memberDto);
        }
    }

생성자 dto 보다의 장점은 컴파일 시점에 오류를 잡아준다는 것 입니다. 생성자 dto는 런타임 시점에 오류를 잡아주지만, 생성자+@QueryProjection은 컴파일 시점에 오류를 잡아줍니다.

 

위 @QueryProjection은 다 좋지만 2가지 단점이 존재합니다.

  1. QMemberDto와 같이 Q멤버를 만들어야 해야 합니다.
  2. Q멤버로 만드는 경우, dto는 querydsl에 의존성을 가지게 됩니다.
    • 해당 문제는 아키텍쳐 관점에서 문제가 생긴다고 한. 해당 dto를 repository, service, application 계층 모두 사용하는데 querydsl에 의존적인 dto를 모든 계층에서 사용하는데 어떠한 문제가 있을지는 아직 모르겠다,,

 

distinct

    @Test
    public void findDtoByQueryProjection() {
        List<MemberDto> result = queryFactory
                .select(new QMemberDto(member.username, member.age)).distinct()
                .from(member)
                .fetch();

        for (MemberDto memberDto : result) {
            System.out.println("memberDto = " + memberDto);
        }
    }

이렇게 select뒤에 distinct 적어주면된다.

 

 

 

동적 쿼리 - BooleanBuilder 사용


    @Test
    public void dynamicQuery_BooleanBuilder() {
        String usernameParam = "member1";
        Integer ageParam = 10;

        List<Member> result = searchMember1(usernameParam, ageParam);
        assertThat(result.size()).isEqualTo(1);
    }

    private List<Member> searchMember1(String usernameCond, Integer ageCond) {
        BooleanBuilder builder = new BooleanBuilder();
        if(usernameCond != null){
            builder.and(member.username.eq(usernameCond));
        }

        if(ageCond != null){
            builder.and(member.age.eq(ageCond));
        }

        return queryFactory
                .selectFrom(member)
                .where(builder)
                .fetch();
    }

위와 같이 null이 아닌 경우에만 builder.and(where안에 넣을 조건)을 넣어주면 된다.

 

 

 

동적 쿼리 - Where 다중 파라미터 사용


김영한님이 즐겨쓰는 방식이라고 한다. 

    @Test
    public void dynamicQuery_whereParam() {
        String usernameParam = "member1";
        Integer ageParam = 10;

        List<Member> result = searchMember2(usernameParam, ageParam);
        assertThat(result.size()).isEqualTo(1);
    }

    private List<Member> searchMember2(String usernameCond, Integer ageCond){
        return queryFactory
                .selectFrom(member)
                .where(usernameEq(usernameCond), ageEq(ageCond))
                .fetch();
    }

    private Predicate usernameEq(String usernameCond) {
        return usernameCond == null ? null : member.username.eq(usernameCond);
    }

    private Predicate ageEq(Integer ageCond) {
        return ageCond == null ? null : member.age.eq(ageCond);
    }

이 기능의 장점은 메소드라 빠지기 때문에, 아래와 같이 조립이 가능하다.

    @Test
    public void dynamicQuery_whereParam() {
        String usernameParam = "member1";
        Integer ageParam = 10;

        List<Member> result = searchMember2(usernameParam, ageParam);
        assertThat(result.size()).isEqualTo(1);
    }

    private List<Member> searchMember2(String usernameCond, Integer ageCond){
        return queryFactory
                .selectFrom(member)
                .where(allEq(usernameCond, ageCond))
                .fetch();
    }

    private BooleanExpression usernameEq(String usernameCond) {
        return usernameCond == null ? null : member.username.eq(usernameCond);
    }

    private BooleanExpression ageEq(Integer ageCond) {
        return ageCond == null ? null : member.age.eq(ageCond);
    }

    private BooleanExpression allEq(String usernameCond, Integer ageCond){
        return usernameEq(usernameCond).and(ageEq(ageCond));
    }
  • where 조건에 null 값은 무시된다.
  • 메서드를 다른 쿼리에서도 재활용 할 수 있다. (이 점이 정말 좋은 것 같다.)
  • 쿼리 자체의 가독성이 높아진다. (이전 BooleanBuilder보다 가독성이 뛰어나다.)

 

 

수정, 삭제 배치 쿼리


쿼리 한번으로 대량 데이터 수정

    @Test
    public void bulkUpdate() {
        // member1 = 10 = > 비회원
        // member2 = 20 = > 비회원
        // member1 = 30 = > 그대로
        // member2 = 40 = > 그대로

        long count = queryFactory
                .update(member)
                .set(member.username, "비회원")
                .where(member.age.lt(28))
                .execute();
    }

두 컬럼의 데이터를 update했지만, 쿼리는 한번만 나간 것을 볼 수 있다.

참고
벌크 연산은 이전에도 말했지만, 조심해야할 것이 있다.
영속성 컨텍스트를 무시하고 DB에 직접 접근해서 update 쿼리문을 날리기 때문이다. 이것이 문제가 되는 것은 영속성 컨텍스트에 update쿼리문에 날라간 데이터가 있고, 벌크 연산 후 데이터를 가져오면 DB가 아닌 영속성 컨텍스트에서 가져오기 때문에 벌크연산된 데이터가 아닌 이전의 데이터를 가져온다. 그렇기 때문에 벌크 연산후에는 em.clear()를 해주자.

 

쿼리 한번으로 대량 데이터 추가, 곱, 삭제

    @Test
    public void bulkAdd() {
        long count = queryFactory
                .update(member)
                .set(member.age, member.age.add(1)) //곱하기는 add 대신 multiply
                .execute();
    }

    @Test
    public void bulkDelete() {
        long count = queryFactory
                .delete(member)
                .where(member.age.gt(18))
                .execute();
    }

해당 벌크 연산도 벌크 연산 후 em.clear()를 필수로 해주어야 한다.

 

 

 

SQL function 호출하기


SQL function은 JPQ와 같이 Dialect에 등록된 내용만 호출할 수 있다.

 

아래 예시는 member string을 m string으로 replace하는 함수를 사용한 예제이다.

    @Test
    public void slqFunction() {
        List<String> result = queryFactory
                .select(Expressions.stringTemplate("function('replace', {0}, {1}, {2})",
                        member.username, "member", "m"))
                .from(member)
                .fetch();

        for (String s : result) {
            System.out.println("result = " + s);
        }
    }

replace말고도 소문자로 변환하는 경우는 아래예시이다.

    @Test
    public void sqlFunction2() {
        List<String> result = queryFactory
                .select(member.username)
                .from(member)
                .where(member.username.eq(Expressions.stringTemplate("function('upper', {0})", member.username)))
                //.where(member.username.eq(member.username.upper()))
                .fetch();

        for (String s : result) {
            System.out.println("result = " + s);
        }
    }

비교할 데이터를 모두 소문자로 비교한 뒤, 비교할 때는 위와같이 소문자로 변경한 뒤 비교할 수 있다.

 

참고로, lower 같은 ansi 표준 함수들은 querydsl이 상당부분 내장하고 있다. 따라서 아래와 같이 처리해도 결과는 같다.

.where(member.username.eq(member.username.upper()))

 

 

 

 

 

출처

 

실전! Querydsl - 인프런 | 강의

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

www.inflearn.com

 

예제 도메인 모델은 Spring Data JPA에서 사용한 모델링이랑 같다.

 

 

JPQL vs Querydsl


    @Test
    public void startJPQL() throws Exception {
        Member findByJPQL = em.createQuery("select m from Member m where m.username = :username", Member.class)
                .setParameter("username", "member1")
                .getSingleResult();

        Assertions.assertThat(findByJPQL.getUsername()).isEqualTo("member1");

    }

    @Test
    public void startQuerydsl() {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        QMember m = QMember.member;

        Member findByQuerydsl = queryFactory.select(m)
                .from(m)
                .where(m.username.eq("member1"))
                .fetchOne();

        Assertions.assertThat(findByQuerydsl.getUsername()).isEqualTo("member1");
    }

JPQL과 Querydsl 테스트하는 코드의 차이이다.

 

 

Querydsl 장점

  • 컴파일시점에 오류를 잡아낸다. (JPQL은 메소드가 실행될 때 오류(Runtime Error)를 발견)
  • 자바 코드로 쿼리를 작성할 수 있다. 
  • 파라미터 바인딩을 자동으로 해결해 준다.
  • Code Assistant도 잘되어있다.

 

 

기본 Q-Type 활용


Q클래스 인스턴스를 사용하는 2가지 방법

        QMember m = new QMember("m");
        QMember m = QMember.member;

참고로 QMember.member은 QMember객체에 만들어놓은 객체이다.

@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QMember extends EntityPathBase<Member> {
    ...
    public static final QMember member = new QMember("member1");
}

 

코드를 더 심플하게 하는 방법

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

위와 같이 import를 해주면 아래와 같이 코드를 만들 수 있다.

        JPAQueryFactory queryFactory = new JPAQueryFactory(em);

        Member findByQuerydsl = queryFactory.select(member)
                .from(member)
                .where(member.username.eq("member1"))
                .fetchOne();

 

Querydsl 쿼리문 보는법

application.yml에 아래와 같이 옵션을 추가해주어야한다.

spring:
  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 #querydsl 쿼리문 보는 옵션

 

 

 

검색 조건 쿼리


기본 검색 쿼리

    @Test
    public void search() {
        Member findMember = queryFactory
                .selectFrom(member)
                .where(member.username.eq("member1")
                        .and(member.age.eq(10)))
                .fetchOne();
                
        assertThat(findMember.getUsername()).isEqualTo("member1");
    }

기본 검색 조건은 .and(), or()를 메서드 체인으로 연결할 수 있다.

select, from을 selectfrom으로 합칠 수 있음.

 

JPQL이 제공하는 모든 검색 조건 제공

member.username.eq("member1") // username = 'member1'
member.username.ne("member1") //username != 'member1'
member.username.eq("member1").not() // username != 'member1'

member.username.isNotNull() //이름이 is not null

member.age.in(10, 20) // age in (10,20)
member.age.notIn(10, 20) // age not in (10, 20)
member.age.between(10,30) //between 10, 30

member.age.goe(30) // age >= 30
member.age.gt(30) // age > 30
member.age.loe(30) // age <= 30
member.age.lt(30) // age < 30

member.username.like("member%") //like 검색 
member.username.contains("member") // like ‘%member%’ 검색 
member.username.startsWith("member") //like ‘member%’ 검색

AND 조건을 파라미터로 처리

    @Test
    public void searchAndParam() {
        List<Member> result1 = queryFactory
                .selectFrom(member)
                .where(member.username.eq("member1"),
                        member.age.eq(10))
                .fetch();
        assertThat(result1.size()).isEqualTo(1);
    }

where()에 파라미터로 검색 조건을 추가하면 AND조건이 추가된다.

 

 

 

결과 조회


  • fetch()
    • list 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne()
    • 단건 조회
    • 결과가 없으면 Null
    • 결과가 둘 이상이면 : NonUniqueResultException
  • fetchFirst()
    • limit(1).fetchOne()
  • fetchResults()
    • 페이징 정보 포함, total count 쿼리 추가 발생
  • fetchCount()
    • count 쿼리로 변경해서 count 수 조회
//List
List<Member> fetch = queryFactory
          .selectFrom(member)
          .fetch();

//단 건
Member findMember1 = queryFactory
          .selectFrom(member)
          .fetchOne();
          
//처음 한 건 조회
Member findMember2 = queryFactory
          .selectFrom(member)
          .fetchFirst();
          
//페이징에서 사용
QueryResults<Member> results = queryFactory
          .selectFrom(member)
          .fetchResults();
          
//count 쿼리로 변경
long count = queryFactory
          .selectFrom(member)
          .fetchCount();

 

 

 

정렬


   /**
     * 회원 정렬 순서
     * 1. 회원 나이 내림차순(desc)
     * 2. 회원 이름 올림차순(asc)
     * 단, 2에서 회원 이름이 없으면 마지막에 출력(nulls last)
     */
    @Test
    public void sort() throws Exception {
        //given
        em.persist(new Member(null, 100));
        em.persist(new Member("member5", 100));
        em.persist(new Member("member6", 100));

        //when
        List<Member> result = queryFactory
                .selectFrom(member)
                .where(member.age.eq(100))
                .orderBy(member.age.desc(), member.username.asc().nullsLast())
                .fetch();

        //then
        Member member5 = result.get(0);
        Member member6 = result.get(1);
        Member memberNull = result.get(2);

        Assertions.assertThat(member5.getUsername()).isEqualTo("member5");
        Assertions.assertThat(member6.getUsername()).isEqualTo("member6");
        Assertions.assertThat(memberNull.getUsername()).isNull();
    }

위와 같이 orderBy안에 파라미터로 정렬 할 수 있다.

 

 

페이징


조회 건수 제한

    @Test
    public void paging(){
        List<Member> result = queryFactory
                .selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1)      // 0부터 시작
                .limit(2)       // 최대 2건 조회
                .fetch();

        assertThat(result.size()).isEqualTo(2);
    }

전체 조회 수가 필요하다면??

fetch() 대신 fetchResults()로 바꿔주면 된다.
주의
count 쿼리가 실행되니 성능상 주의해야 한다.
성능이 나오지 않는다면, count 쿼리를 분리해야 한다.

 

 

 

집합


집합 함수

    @Test
    public void aggregation() {
        List<Tuple> result = queryFactory
                .select(
                        member.count(),     // 개수
                        member.age.sum(),   // 합
                        member.age.avg(),   // 평균
                        member.age.max(),   // 가장 큰 값
                        member.age.min())   // 가장 작은 값
                .from(member)
                .fetch();

        //Tuple은 querydsl이 제공하는 값
        Tuple tuple = result.get(0);
        assertThat(tuple.get(member.count())).isEqualTo(4);
        assertThat(tuple.get(member.age.sum())).isEqualTo(100);
        assertThat(tuple.get(member.age.avg())).isEqualTo(25);
        assertThat(tuple.get(member.age.max())).isEqualTo(40);
        assertThat(tuple.get(member.age.min())).isEqualTo(10);
    }

위와 같이 java 코드처럼 사용가능하다.

 

GroupBy

    @Test
    public void group() throws Exception {
        //given
        List<Tuple> result = queryFactory
                .select(team.name, member.age.avg())
                .from(member)
                .join(member.team, team)
                .groupBy(team.name)     // 팀 이름으로 그룹
                .fetch();

        //when
        Tuple teamA = result.get(0);
        Tuple teamB = result.get(1);

        //then
        assertThat(teamA.get(team.name)).isEqualTo("teamA");
        assertThat(teamA.get(member.age.avg())).isEqualTo(15);
        assertThat(teamB.get(team.name)).isEqualTo("teamB");
        assertThat(teamB.get(member.age.avg())).isEqualTo(35);
    }

groupBy, 그룹화된 결과를 제한하려면 having

 

GroupBy, having

                .groupBy(team.name)     // 팀 이름으로 그룹
                .having(team.name.eq("teamB"))

위처럼 "teamB"와 같은 team만 가져올 수 있습니다.

 

 

 

조인 - 기본 조인


    /**
     * 팀 A에 소속된 모든 회원
     */
    @Test
    public void join() throws Exception {
        List<Member> result = queryFactory
                .selectFrom(member)
                .join(member.team, team)
                .where(team.name.eq("teamA"))
                .fetch();

        assertThat(result).extracting("username").containsExactly("member1", "member2");
    }

위의 방식으로 join을 사용할 수 있다. leftjoin, rightjoin 모두 사용 가능하다.

연관관계가 없는 경우 from 절에 모든 데이터를 가져온 뒤, where 문으로 theta join할 수 있다.

 

 

조인 - on절


on절을 활용한 조인

  1. 조인 대상 필터링
  2. 연관관계 없는 엔티티 외부 조인

 

조인 대상 필터링

    /**
     * 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
     * jpql : select m, t from Member m left join m.team t on t.name = "teamA"
     */
    @Test
    public void join_on_filtering() throws Exception {
        List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .join(member.team, team)
                .on(team.name.eq("teamA"))
                .fetch();

        for(Tuple tuple : result){
            System.out.println("tuple = " + tuple);
        }
    }

 

연관관계 없는 엔티티 외부 조인

    /**
     * 연관관계가 없는 엔티티 외부 조인
     * 회원의 이름이 팀 이름과 같은 대상 외부 조인
     * jpql : select m, t from Member m LEFT JOIN Team t on m.username = t.name
     */
    @Test
    public void join_on_no_relation() throws Exception {
        em.persist(new Member("teamA"));
        em.persist(new Member("teamB"));
        em.persist(new Member("teamC"));

        List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .leftJoin(team)
                .on(member.username.eq(team.name))
                .fetch();

        for(Tuple tuple : result){
            System.out.println("tuple = " + tuple);
        }
    }

주의해야 할 점은 leftjoin() 부분에 일반 조인과 다르게 엔티티가 하나만 들어갔다.

  • 일반 조인 : leftJoin(member.team, team)
  • on 조인 : from(member).leftJoin(team).on(~~~)

위의 방식으로 조인 문법이 다르다.

 

 

 

조인 - 페치 조인


    @Test
    public void fetchjoinUse() throws Exception {
        em.flush();
        em.clear();

        Member findMember = queryFactory
                .selectFrom(member)
                .join(member.team, team).fetchJoin()
                .where(member.username.eq("member1"))
                .fetchOne();

        boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
        assertThat(loaded).as("페치조인 미적용").isTrue();
    }

join().fetchJoin() 을사용하면 fetch join이 된다.

 

 

 

서브 쿼리


com.querydsl.jpa.JPAExpressions 사용

 

서브 쿼리 eq 사용

    /**
     * 나이가 가장 많은 회원 조회
     */
    @Test
    public void subQuery() {
        QMember memberSub = new QMember("memberSub");

        List<Member> result = queryFactory
                .selectFrom(member)
                .where(member.age.eq(
                        JPAExpressions
                                .select(memberSub.age.max())
                                .from(memberSub)
                ))
                .fetch();

        assertThat(result.get(0).getAge()).isEqualTo(40);
    }

같은 QType을 쓰면안되기때문에, 서브쿼리에 넣을 QType은 새롭게 만들어서 넣어줘야 한다.

 

서브 쿼리 goe 사용

    /**
     * 나이가 평균 이상인 회원
     */
    @Test
    public void subQueryGoe() {
        QMember memberSub = new QMember("memberSub");

        List<Member> result = queryFactory
                .selectFrom(member)
                .where(member.age.goe(
                        JPAExpressions
                                .select(memberSub.age.avg())
                                .from(memberSub)
                ))
                .fetch();

        assertThat(result.size()).isEqualTo(2);
    }

goe는 >= 라고 보면된다.

 

서브 쿼리 In 사용

    /**
     * 나이가 10 초과인 회원
     */
    @Test
    public void subQueryIn() {
        QMember memberSub = new QMember("memberSub");

        List<Member> result = queryFactory
                .selectFrom(member)
                .where(member.age.in(
                        JPAExpressions
                                .select(memberSub.age)
                                .from(memberSub)
                                .where(memberSub.age.gt(10))
                ))
                .fetch();

        assertThat(result.size()).isEqualTo(3);
    }

 

select 절에 subquery

    /**
     * 나이가 10 초과인 회원
     */
    @Test
    public void selectSubQuery() {
        QMember memberSub = new QMember("memberSub");

        List<Tuple> result = queryFactory
                .select(member.username,
                        JPAExpressions
                                .select(memberSub.age.avg())
                                .from(memberSub))
                .from(member)
                .fetch();

        for(Tuple tuple : result){
            System.out.println("tuple = " + tuple);
        }
    }

 

from 절의 서브쿼리 한계

JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다. 

 

from 절의 서브쿼리 해결방안

  • 서쿼리를 Join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다고 한다.)
  • 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
  • nativeSQL을 사용한다.

from절의 서브 쿼리를 줄이기 위한 책을 추천해주었다. SQL AntiPattern 나중에 읽을 기회가 된다면 사서 봐보자.

 

 

 

Case 문


    @Test
    public void basicCase() {
        List<String> result = queryFactory
                .select(member.age.when(10).then("열살")
                        .when(20).then("스무살")
                        .otherwise("서른살 이상"))
                .from(member)
                .fetch();

        for(String s : result){
            System.out.println("s = " + s);
        }
    }

    @Test
    public void complexCase() {
        List<String> result = queryFactory
                .select(new CaseBuilder()
                        .when(member.age.between(0, 20)).then("0~20살")
                        .when(member.age.between(21, 30)).then("21~30살")
                        .otherwise("기타"))
                .from(member)
                .fetch();

        for(String s : result){
            System.out.println("s = " + s);
        }
    }

간단한 case문 예제이다.

 

orderyBy에서 Case문 함께 사용하기

 

예를들어 아래와 같은 임의의 순서로 회원을 출력하고 싶다면?

  1. 0~30살이 아닌 회원을 가장 먼저 출력
  2. 0~20살 회원 출력
  3. 21~30살 회원 출력
    @Test
    public void addCase() {
        NumberExpression<Integer> rankPath = new CaseBuilder()
                .when(member.age.between(0, 20)).then(2)
                .when(member.age.between(21, 30)).then(1)
                .otherwise(3);

        List<Tuple> result = queryFactory
                .select(member.username, member.age, rankPath)
                .from(member)
                .orderBy(rankPath.desc())
                .fetch();

        for (Tuple tuple : result) {
            String username = tuple.get(member.username);
            Integer age = tuple.get(member.age);
            Integer rank = tuple.get(rankPath);
            System.out.println("username = " + username + " age = " + age + " rank = "
                    + rank); 
        }
    }

Querydsl은 자바 코드로 작성하기 때문에 rankPath처럼 복잡한 조건을 변수로 선언해서 select절, orderBy절에서 사용할 수 있다.

 

 

 

상수, 문자 더하기


상수 더하기

    /**
     * 상수 더하기
     */
    @Test
    public void constant() {
        List<Tuple> result = queryFactory
                .select(member.username, Expressions.constant("A"))
                .from(member)
                .fetch();

        for(Tuple tuple : result){
            System.out.println("tuple = " + tuple);
        }
    }

Expressions.constant()를 사용하면 된다.

 

문자 더하기

    /**
     * 문자 더하기
     */
    @Test
    public void concat() {
        //{username}_{age}
        List<String> result = queryFactory
                .select(member.username.concat("_").concat(member.age.stringValue()))
                .from(member)
                .where(member.username.eq("member1"))
                .fetch();

        for(String s : result){
            System.out.println("s = " + s);
        }
    }

가져온 값을 .stringValue()를 사용하며 string 변환하면 더할 수 있다.

 

 

 

 

 

 

 

 

출처

 

실전! Querydsl - 인프런 | 강의

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

www.inflearn.com

 

build.gralde 코드 추가 (주석된 부분 추가)

buildscript {
	ext {
		queryDslVersion = "5.0.0"
	}
}

plugins {
	id 'org.springframework.boot' version '2.6.7'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	//querydsl 추가
	id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
	id 'java'
}

group = 'study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2'
	//querydsl 추가
	implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
	implementation "com.querydsl:querydsl-apt:${queryDslVersion}"


	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
	useJUnitPlatform()
}

//querydsl 추가 시작
def querydslDir = "$buildDir/generated/querydsl"

querydsl {
	jpa = true
	querydslSourcesDir = querydslDir
}
sourceSets {
	main.java.srcDir querydslDir
}
compileQuerydsl{
	options.annotationProcessorPath = configurations.querydsl
}
configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
	querydsl.extendsFrom compileClasspath
}
//querydsl 추가 끝

검증용 엔티티 생성 (Hello Class)

package study.querydsl.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
@Getter
@Setter
public class Hello {

    @Id @GeneratedValue
    private Long id;
}

후에 오른쪽 gradle 클릭 (없으면 왼쪽 맨아래 네모난 박스 클릭 마우스 이동 후 gradle 클릭) ->  querydsl 클릭 -> task 클릭 -> other 클릭 -> compileQuerydsl 더블 클릭

참고
위의 작업없이 build만 해도 되긴함.

 

후에 build>generated>querydsl>study.qeurydsl.entity를 보면 QHello가 생선된 걸 볼 수 있다.

QHello 코드

package study.querydsl.entity;

import static com.querydsl.core.types.PathMetadataFactory.*;

import com.querydsl.core.types.dsl.*;

import com.querydsl.core.types.PathMetadata;
import javax.annotation.processing.Generated;
import com.querydsl.core.types.Path;


/**
 * QHello is a Querydsl query type for Hello
 */
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QHello extends EntityPathBase<Hello> {

    private static final long serialVersionUID = 1910216155L;

    public static final QHello hello = new QHello("hello");

    public final NumberPath<Long> id = createNumber("id", Long.class);

    public QHello(String variable) {
        super(Hello.class, forVariable(variable));
    }

    public QHello(Path<? extends Hello> path) {
        super(path.getType(), path.getMetadata());
    }

    public QHello(PathMetadata metadata) {
        super(Hello.class, metadata);
    }

}
Hello 엔티티를 보고 querydsl이 QHello란 엔티티를 만들어준 것이다.
QHello는 querydsl에서 쿼리와 관련된 작업을 할 때 사용하기위해 만들어지는 엔티티이다.
참고
querydsl이 만들어준 QHello와 같은 엔티티는 git에서 관리하면 안된다. (자동으로 gitignore되는 폴더에 생성되기때문에 상관없긴하다.)

 

 

 

출처

 

실전! Querydsl - 인프런 | 강의

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

www.inflearn.com

 

 

Spring Data JPA가 아닌 다른 이유로 인터페이스의 메서드를 직접 구현하고 싶을 때 사용하면 된다. 예를들어, JPA 직접 사용, 스프링 JDBC Template사용, MyBatis사용, Querydsl사용 등등이 있다.

 

사용자 정의 리포지토리 구현


먼저, 인터페이스를 만들어야 한다.
MemberRepositoryCustom Interface 생성

package study.datajpa.repository;

import study.datajpa.entity.Member;
import java.util.*;

public interface MemberRepositoryCustom {
    List<Member> findMemberCustom();
}

그리고 인터페이스를 구현할 구현체 클래스를 만들어준다.

MemberRepositoryCustomImpl class 생성

package study.datajpa.repository;

import lombok.RequiredArgsConstructor;
import study.datajpa.entity.Member;

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

@RequiredArgsConstructor
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom{
    private final EntityManager em;

    @Override
    public List<Member> findMemberCustom() {
        return em.createQuery("select m From Member m")
                .getResultList();
    }
}

그리고 Spring Data JPA 클래스에 인터페이스를 상속하면 된다.

MemberRepository 코드 추가 (인터페이스 상속)

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
	...
}

이 기능은 Java에서 제공해주는 것이 아닌, Spring Data JPA에서 제공해주는 기능이다. 특히, 김영한님은 복잡해지는 쿼리들을 querydsl을 쓰기 위해 커스텀해서 사용한다고 한다.

 

 

해당 기능을 사용할 때 규칙이 있다.

  • 규칙 : 리포지토리 인터페이스 이름 + Impl
  • 규칙을 지켜야 Spring Data JPA가 인식해서 스프링 빈으로 등록한다고 한다.
  • "리포지토리 인터페이스 이름" + "아무값" + "Impl" 적어도 인식하여 등록해주는 것 같다.
변경하는 방법도 있는데, 왜만하면 관례를 따르는게 유지보수성면에서 좋아보인다.

 

 

 

Auditing


엔티티 생성, 변경할 때 변경한 사람과 시간을 추적하고 싶을 때 사용

  • 등록일
  • 수정일
  • 등록자
  • 수정자

기본적으로 테이블을 만들 때, 등록일/수정일을 만들어야 운영에서 지옥을 맛보지 않는다고 한다. 추적이 되지 않기 때문이라고한다. 언제 등록이 됐고, 언제 수정이 됐는지는 기본으로 깔고가는 습관을 들여보자!

 

순수 JPA 사용

JpaBaseEntity 클래스 생성

package study.datajpa.entity;

import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import java.time.LocalDateTime;

@MappedSuperclass
public class JpaBaseEntity {

    @Column(updatable = false)
    private LocalDateTime createdDate;
    private LocalDateTime updatedDate;

    @PrePersist     // persist하기전에 event
    public void prePersist() {
        LocalDateTime now = LocalDateTime.now();
        createdDate = now;
        updatedDate = now;
    }

    @PreUpdate      // update하기전에 event
    public void preUpdate() {
        updatedDate = LocalDateTime.now();
    }
}

Member 엔티티에 상속

public class Member extends JpaBaseEntity {
	...
}

Table 생성 쿼리

 

 

Spring Data JPA 사용

Main 클래스에 @EnableJpaAuditing, 등록자/수정자 메소드 추가

@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
	...
    
    // 등록자, 수정자 값 가져오는 메소드
	@Bean
	public AuditorAware<String> auditorProvider() {
		return () -> Optional.of(UUID.randomUUID().toString());
	}
}

BaseEntity 클래스 생성

package study.datajpa.entity;

import lombok.Getter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {

    // 등록일
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;

    // 수정일
    @LastModifiedDate
    private LocalDateTime lastModifiedDate;

    // 등록자
    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    // 수정자
    @LastModifiedBy
    private String lastModifiedBy;
}

 

 

 

Web 확장 - 페이징과 정렬


 

Web에서 페이징과 정렬을 하는 방법이다.

 

MemberController 메소드 추가

public class MemberController {
    ...
    @GetMapping("/members")
    public Page<Member> list(Pageable pageable){
        Page<Member> page = memberRepository.findAll(pageable);
        return page;
    }

    @PostConstruct
    public void init() {
        for(int i = 0; i < 100; i++){
            memberRepository.save(new Member("user"+i, i));
        }
    }
}

 

파라미터에 Pageable을 추가하면 페이징과 sort기능을 사용할 수 있다. Page<Member>로 반환값을 하면 토탈값을 알 수 있다.

위처럼 url+"?page={페이지번호}&size={원하는사이즈}&sort={sort하고싶은 컬럼명},desc(기본 asc)"
을 사용하면 페이징과 정렬이 가능하다. 

 

기본이 page별 사이즈가 20인데 apllication.yml, 파라미터로 수정할 수 있다.

application.yml

  data:
    web:
      pageable:
        default-page-size: 10
        max-page-size: 2000

파라미터

    @GetMapping("/members")
    public Page<Member> list(@PageableDefault(size = 5) Pageable pageable){
        Page<Member> page = memberRepository.findAll(pageable);
        return page;
    }

 

 

접두사

페이징 정보가 둘 이상이면 접두사로 구분한다.
  • @Qualifier 에 접두사명 추가 "(접두사명)_xxx"
  • 예) /members?member_page=0&order_page=1 
  public String list(
      @Qualifier("member") Pageable memberPageable,
      @Qualifier("order") Pageable orderPageable, ...
  }

 

 

Dto 반환

    @GetMapping("/members")
    public Page<MemberDto> list(@PageableDefault(size = 5) Pageable pageable){
        return memberRepository.findAll(pageable)
                .map(member -> new MemberDto(member));
    }

위와 같이 page에서 제공해주는 기능을 이용하면 된다.

 

 

 

 

 

 

 

출처

 

실전! 스프링 데이터 JPA - 인프런 | 강의

스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제공합니다.

www.inflearn.com

 

@Query(value = "select m from Member m left join m.team t",
        countQuery = "select count(m) from Member m")
Page<Member> findByAge(int age, Pageable pagealbe);

이전에 만들었던 interface Repository에 username으로 조회하는 기능을 추가하고 싶을 때는 어떻게해야 될까??

interface이기 때문에 새로운 기능을 추가 하기 어렵다. 그렇다고 하나의 기능을 추가하기 위해 새로운 클래스를 만들고 interface를 상속받으면 아래와 같이 모든 메소드를 Override 해야하고 기능을 구현해야 한다. (이렇게 되는 경우 Spring Data JPA를 사용하는 의미가 없어진다.)

 

위와같은 문제를 해결 하기 위한 것이 쿼리 메소드 기능이다.

 

 

쿼리 메소드 기능


 

메소드 이름으로 쿼리 생성


    public List<Member> findByUsernameAndAgeGreaterThan(String username, int age){
        return em.createQuery("select m from Member m where m.username = :username " +
                "and m.age > :age")
                .setParameter("username", username)
                .setParameter("age", age)
                .getResultList();
    }

위와 같이 파라미터로 넘어온 username이 같고, age보다 높은 컬럼들을 반환하는 기능을 JPA로 구현한 코드입니다.

 

이를 Spring Data JPA에 추가하려면 아래와 같이 하면됩니다.

List<Member> findByUsernameAndAgeGreaterThan(String username, int age);

메소드 이름으로 쿼리를 작성해주는 Spring Data JPA의 강력한 기능 때문에 위의 코드 한줄로 같은 기능을 제공해줍니다.

username뒤에는 아무런 기능이 없기 때문에 equal(=).  And 후
age뒤에는 GreaterThan이 있습니다. 즉, 파라미터인 age보다 크면 (>)
반환해주는 쿼리문을 Spring Data JPA에서 작성해줍니다.
주의 
메소드로 쿼리문을 작성하기 때문에 오타가 나면 안됩니다!! 

 

쿼리 메소드 필터 조건

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

확인해보니 대부분의 기능들을 제공해줍니다. 하지만, 2개가 넘어가는 경우 가독성이 떨어지는 우려가 생기고 오타가 발생할 수 있는 문제점이 있을 것 같습니다. 

 

 

스프링 데이터 JPA가 제공하는 쿼리 메소드 기능

 

 

 

메소드 이름으로 쿼리 생성 정리

  • 장점
    • 애플리케이션 로딩 시점에 오류를 인지할 수 있다
  • 단점
    • 엔티티의 필드명이 변경되는 경우 메서드 이름도 함께 변경해야한다. 즉, 유지보수성이 뛰어나지는 않다.

 

 

 

 

@Query, 리포지토리 메소드에 쿼리 정의하기


메소드 위에 @Query("쿼리문")을 통해 사용할 수 있다.

    @Query("select m from Member m where m.username = :username and m.age > :age")
    List<Member> findUser(@Param("username") String username, @Param("age") int age);

이전에 사용했던 

    List<Member> findByUsernameAndAgeGreaterThan(String username, int age);

메소드 이름으로 작성했던 쿼리문이랑 같은 기능을 한다. 

 

 

 

@Query 정리

  • 장점
    • 애플리케이션 로딩 시점에 정적 쿼리이기때문에 SQL로 파싱을 해놓는다고 합니다. 그렇기때문에 애플리케이션 로딩시점에 문법 오류를 알 수 있습니다.
    • DTO로 반환을 할 수 있다.
  • 단점
    • 간단한 쿼리문인 경우 메소드 이름으로 작성하는 것보다 많은 코드를 써야 한다. (간단한 쿼리는 메소드 이름 사용하면 편리할듯? 합니다.)
    • 동적 쿼리를 작성할 수 없다. 동적 쿼리는 QueryDSL가 좋다고 한다.

 

 

 

@Query, 값, DTO 조회하기


기존 JPQL에서 dto로 반환했을 때처럼 사용하면됩니다.

MemberRepository 코드 추가

    @Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
    List<MemberDto> findMemberDto();

위와 같이 코드를 작성하면 @Query를 통해 Dto로 반환할 수 있습니다.

 

 

 

 

파라미터 바인딩


위치 기반, 이름 기반 파라미터 바인딩은 지금까지 해왔던 바인딩이다. 파라미터 바인딩에는 컬렉션 파라미터 바인딩이라는 것도 있다.

 

컬렉션 파라미터 바인딩

Collection 타입으로 in절 지원

    @Query("select m from Member m where m.username in :names")
    List<Member> findByNames(@Param("names") Collection<String> names);

위처럼 in 절을 사용해서 컬렉션 파라미터 바인딩을 지원해줍니다.

 

 

 

반환 타입


단일 건의 조회일 때, null 값이 넘어올 가능성이 있다면 Optional을 사용하자! (NullPointException 방지!) 

 

 

 

 

 

순수 JPA 페이징과 정렬


페이징과 정렬 파라미터

  • 정렬
    • org.springframework.data.domain.Sort
  • 페이징 기능 (내부에 Sort 포함)
    • org.springframework.data.domain.pageable

 

특별한 반환 타입

  • 추가 Count 쿼리 결과를 포함하는 페이징
    • org.springframework.data.domain.Page
  • 추가 Count 쿼리 없이 다음 페이지만 확인 가능(내부적으로 Limit +1 조회)
    • org.springframework.data.domain.Slice

 

Spring Data JPA 페이지 사용

    Page<Member> findByAge(int age, Pageable pagealbe);

이전에 배웠던 메소드 이름으로 쿼리를 생성해주면서 페이징을 해준다. (파라미터로 Pageable을 추가해주면 사용이 가능하다.)

반환값을 Page로 하는 경우 (추가 count 쿼리 등을 할 수 있다.)

 

Test

        //given
        int age = 10;
        PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

        //when
        Page<Member> page = memberRepository.findByAge(age, pageRequest);

        //then
        List<Member> content = page.getContent();

Pageable, Page<T> 사용방법

Page<T>의 경우 getTotalElements(), getNumber(), getTotalPages() 등등의 기능이 있으니 공부해서 사용해보면 좋을 것 같다.

참고로, Slice<T> 반환의 경우 getTotalElements(), getTotalPages()와 같은 totalCount와 관련된 기능만 제외(Pgae<T> 에서)됩니다.

 

반환값에 따른 쿼리문 변화

Page<Member> (왼쪽)                 /              List<Member>, Slice<Member>  (오른쪽) 

 

Page<T> 정리

  • 장점
    • 반환 값을 Page<T>만 바꿔주어도 total count 쿼리문을 보낼 수 있다.
  • 단점
    • Total count 쿼리문이 나가기때문에 성능상 좋지 않다. (컬럼 수가 많을 때 특히 비효율적)

 

Page<T>로 반환 할 때, join을 하는경우 count 쿼리문도 조인을 하기 때문에 countQuery문을 따로 작성하는 것이 성능에 좋다. (최선)

CountQuery X

@Query(value = "select m from Member m left join m.team t")
Page<Member> findByAge(int age, Pageable pagealbe);

발생한 쿼리문

CountQuery O

@Query(value = "select m from Member m left join m.team t",
        countQuery = "select count(m) from Member m")
Page<Member> findByAge(int age, Pageable pagealbe);

발생한 쿼리문

조인을하며 쿼리가 복잡해지는 경우 성능을 높이기 위해 CountQuery문을 따로 작성하는 방법도 좋을 것 같다.

 

 

Page일 때, Dto로 변환하는 방법

        Page<Member> page = memberRepository.findByAge(age, pageRequest);
        Page<MemberDto> pageDto = page.map(member -> new MemberDto(member.getId(), 
        					member.getUsername(), 
       						member.getTeam().getName()));

위의 방식으로 쉽게 Dto로 변환이 가능하다.

 

주의
page는 1부터 시작이 아닌 0부터 시작이다. (배열 Index와 같음)

 

 

 

벌크성 수정 쿼리


벌크성 수정 쿼리를 사용할 때는 한 번에 많은 컬럼을 수정해야 하는 경우 사용한다. (예를들면, 물가 상승에 따른 모든 물건 값이 10%씩 올랐을 때 한번에 모든 물건의 가격을 10%올리는 쿼리문을 보내야할 때)

 

순수 JPA

    public int bulkAgePlus(int age){
        return em.createQuery("update Member m set m.age = m.age+1 where m.age >= : age")
                .setParameter("age", age)
                .executeUpdate();
    }

파라미터 보다 이상이면 age를 +1하는 쿼리문을 순수한 JPA로 작성했다. 이를 Spring Data JPA로 변형해보자.

 

Spring Data JPA

    @Modifying  //executeUpdate() 와 같다고 보면 됌 
    @Query("update Member m set m.age = m.age + 1 where m.age >= :age")
    int bulkAgePlus(@Param("age") int age);

 

하지만, 벌크 연산에는 문제점이 있다. 벌크 연산하는 경우 영속성 컨텍스트를 거쳐서 update를 하는 것이 아닌 바로 DB에 쿼리문을 날린다고 한다.

 

여기서 발생하는 문제점은 하나의 트랜잭션에서 insert한 엔티티를 벌크연산을 해버린 뒤, insert한 엔티티를 select하여 조회하면 벌크연산이 되지 않는다는 것이다. 

 

설명을 하자면, Insert하면 영속성 컨텍스트와 DB에 저장이 된다. 벌크 연산하면 DB에만 저장이 된다. select하면 DB를 조회하기 전에 영속성 컨텍스트에 찾고자하는 엔티티가 있는지 확인한다. 이때, 있으면 영속성 컨텍스트에서 가져오고 없다면 DB를 조회해서 갖고온다. 하지만 현재 영속성 컨텍스트에 있기때문에 DB를 거치지 않고 반환을 하게 된다. 이렇게 되는 경우 벌크연산이 되지 않은 엔티티가 반환이 되버리고 만다. 이것이 문제가 되는 것이다. (이해가 되지 않는다면, 영속성 컨텍스트에 대한 내용을 공부하는 것을 권장)

 

해결 방법은 벌크 연산 후, em.clear()를 하면 된다. 영속성 컨텍스트가 비어지기때문에 DB에 접근해서 데이터를 갖고오기때문에 위와 같은 문제가 일어나지 않게 된다. 혹은, @Modifying(clearAutomatically = true) 옵션을 추가하면 된다. 쿼리가 나고 바로 영속성 컨텍스트를 clear 시켜주는 옵션이다.

 

 

 

@EntityGraph


Spring Data JPA에서 fetch join을 사용하는 방법

//공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"}) 
List<Member> findAll();

//JPQL + 엔티티 그래프 
@EntityGraph(attributePaths = {"team"}) 
@Query("select m from Member m") 
List<Member> findMemberEntityGraph();

//메서드 이름으로 쿼리에서 특히 편리하다. 
@EntityGraph(attributePaths = {"team"}) 
List<Member> findByUsername(String username)

@EntityGraph를 통해 fetch join을 사용할 수 있다.

 

 

EntityGraph 정리

  • fetch join의 간편 버전이다.
  • left outer join 사용
  • 보통은 JPQL로 사용한다. 간단간단한 경우 EntityGraph를 사용

 

 

 

JPA Hint & Lock


JPA Hint

JPA 쿼리 힌트 (SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트이다.)

 

쿼리 힌트 사용

    @QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
    Member findReadOnlyByUsername(String username);

일반 select를 하게되면, 더티 체킹을 하기위해 1차캐시에 엔티티의 정보를 저장해놓는다. 이것이 성능상 크지는 않지만 작게나마 영향을 줄 수 있다. 그때 QueryHint를 통해 readOnly를 사용하면 1차캐시에 저장하지 않기 때문에 조회만 할때 성능이 좋아질 수 있다. (단, 정말 조회만 할 때 사용해야 한다. 더티체킹을 통한 update 쿼리문이 날라가지 않기 때문이다.)

 

 

Lock

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    List<Member> findByUsername(String name);
JPA가 제공하는 락은 JPA 책 16.1 트랜잭션과 락 절 참고.

깊은 내용이기 때문에 책을 참고하며 공부해야 될 것 같다.

 

 

 

 

 

출처

 

실전! 스프링 데이터 JPA - 인프런 | 강의

스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제공합니다.

www.inflearn.com

 

+ Recent posts