https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84/
실전! Querydsl - 인프런 | 강의
Querydsl의 기초부터 실무 활용까지 한번에 해결, 본 강의는 자바 백엔드 개발의 실전 코스를 완성하는 마지막 강의 입니다. 스프링 부트와 JPA 실무 완전 정복 로드맵을 우선 확인해주세요. 로드
www.inflearn.com
강의를 들으며 생각 정리
여기서 소개하는 기능은 제약이 커서 복잡한 실무 환경에서 사용하기에는 많이 부족하다. 그래도 스프링 데이터에서 제공하는 기능이므로 간단히 소개하고, 왜 부족한지 설명하겠다.
인터페이스 지원 - QuerydslPredicateExecutor
<QuerydslPredicateExecutor 인터페이스>
public interface QuerydslPredicateExecutor<T> {
Optional<T> findById(Predicate predicate);
Iterable<T> findAll(Predicate predicate);
long count(Predicate predicate);
boolean exists(Predicate predicate);
// … more functionality omitted.
}
-> 스프링 데이터 JPA 메서드에 Predicate, 즉 Querydsl의 where 조건을 붙일 수 있다.
<리포지토리에 적용>
public interface MemberRepository extends JpaRepository<Member, Long>, QuerydslPredicateExecutor<Member> {
}
테스트
Iterable result = memberRepository.findAll(
member.age.between(10, 40)
.and(member.username.eq("member1"))
);
<한계점>
1. 조인을 사용할 수 없다.
2. 클라이언트가 Querydsl에 의존해야 한다. 서비스 클래스가 Querydsl이라는 구현 기술에 의존해야 한다.
-> 복잡한 실무환경에서 사용하기에는 한계가 명확하다.
Querydsl Web 지원
스프링 공식 문서 참고
https://docs.spring.io/spring-data/jpa/docs/2.2.3.RELEASE/reference/html/#core.web.type-safe
@Controller
class UserController {
@Autowired UserRepository repository;
@RequestMapping(value = "/", method = RequestMethod.GET)
String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate) {
model.addAttribute("users", repository.findAll(predicate));
return "index";
}
}
@QuerydslPredicate 애노테이션을 통해 요청 파라미터 정보를 predicate로 변환해서 받게 된다.
이를 앞서 배운 QuerydslPredicateExecutor 파라미터로 사용할 수 있다.
<한계점>
1. 단순한 조건만 가능 (거의 eq만 가능하다고 보면 된다)
2. 조건을 커스텀하는 기능이 복잡하고 명시적이지 않다.
3. 컨트롤러가 Querydsl에 의존한다.
-> 복잡한 실무 환경에서 사용하기에는 한계가 명확
리포지토리 지원 - QuerydslRepositorySupport
이전에 만들었던 사용자 정의 리포지토리에 QuerydslRepositorySupport를 적용해보자.
<QuerydslRepositorySupport 적용 전>
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final JPAQueryFactory queryFactory;
public MemberRepositoryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
@Override
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
QueryResults<MemberTeamDto> results = queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name))
.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);
}
}
1. JPAQueryFactory를 사용한다.
2. pageable을 받아서 offset, limit을 계산해 페이징 처리를 한다.
<QuerydslRepositorySupport 적용 후>
public class MemberRepositoryImpl extends QuerydslRepositorySupport implements MemberRepositoryCustom {
public MemberRepositoryImpl() {
super(Member.class);
}
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
JPQLQuery<MemberTeamDto> jpaQuery = from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
)
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name));
JPQLQuery<MemberTeamDto> query = getQuerydsl().applyPagination(pageable, jpaQuery);
QueryResults<MemberTeamDto> results = query.fetchResults();
List<MemberTeamDto> content = results.getResults();
long total = results.getTotal();
return new PageImpl<>(content, pageable, total);
}
}
1. QuerydslRepositorySupport를 상속한다.
2. QuerydslRepositorySupport에 EntityManager가 명시되어 있기 때문에 생성자에 따로 EntityManager를 주입하지 않는다. 대신 super(domainClass)를 생성자 형식으로 가진다.
3. JPAQueryFactory를 사용하지 않는다. 대신 from절부터 시작하는 Querydsl 3.x 버전의 형식을 사용한다.
4. offset, limit을 일일이 계산하지 않고 applyPagination을 통해 페이징 계산을 할 수 있다.
<장점>
1. getQuerydsl().applyPagination(): 스프링 데이터가 제공하는 페이징을 Querydsl로 편리하게 변환 가능
(단, Sort는 오류발생)
2. from()으로 시작 가능
(최근에는 QueryFactory를 사용해서 select()으로 시작하는 것이 더 명시적)
3. EntityManager 제공
<한계점>
1. Querydsl 3.x 버전을 대상으로 만듬 -> Querydsl 4.x에 나온 JPAQueryFactory로 시작할 수 없음 -> select으로 시작할 수 없음 -> from으로 시작해야 함
2. 스프링 데이터 Sort 기능이 정상 동작하지 않음 (일종의 버그)
3. 쿼리를 메서드 한 번에 받지 않고 메서드 체인이 중간에 끊긴다.
(offset, limit 줄이려고 applyPagination()을 사용했으나 결과적으로 코드는 줄어들지 않는다)
Querydsl 지원 클래스 직접 만들기
스프링 데이터가 제공하는 QuerydslRepositorySupport 가 지닌 한계를 극복하기 위해 직접 Querydsl 지원 클래스를 만들어보자.
-> 클래스를 일일이 분석하는 것보다 이런식으로 직접 유틸리티 클래스, 지원 클래스를 만들 수 있다는 관점에서 접근하자.
<Querydsl4RepositorySupport - 직접 만든 클래스>
@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);
}
}
<Querydsl4RepositorySupport 사용 코드>
@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, query -> query
.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
.select(member.id)
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
);
}
private BooleanExpression usernameEq(String username) {
return hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName) {
return hasText(teamName) ? team.name.eq(teamName) : null;
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe != null ? member.age.goe(ageGoe) : null;
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe != null ? member.age.loe(ageLoe) : null;
}
}
Querydsl4RepositorySupport의 applyPagination() 메서드를 통해 보다 편리하게 페이징을 할 수 있다.
'java > querydsl' 카테고리의 다른 글
[Querydsl] 실무 활용 - 스프링 데이터 JPA와 Querydsl (0) | 2021.06.06 |
---|---|
[Querydsl] 실무 활용 - 순수 JPA와 Querydsl (0) | 2021.06.04 |
[Querydsl] 중급 문법 (1) | 2021.06.03 |
[Querydsl] 기본 문법 (0) | 2021.06.02 |
[Querydsl] 예제 도메인 모델 (1) | 2021.06.01 |