Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 스프링 db2 - 데이터 접근 기술
- 게시글 목록 api
- jpa 활용2 - api 개발 고급
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch3
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch9
- 코드로 시작하는 자바 첫걸음
- 스프링 고급 - 스프링 aop
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch2
- 2024 정보처리기사 시나공 필기
- 스프링 입문(무료)
- 자바의 정석 기초편 ch7
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch13
- 자바의 정석 기초편 ch4
- 자바의 정석 기초편 ch1
- 스프링 mvc2 - 타임리프
- 2024 정보처리기사 수제비 실기
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch6
- @Aspect
- 자바의 정석 기초편 ch14
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch12
- 스프링 mvc1 - 서블릿
Archives
- Today
- Total
나구리의 개발공부기록
스프링 데이터 JPA가 제공하는 Querydsl기능, 인터페이스 지원 - QuerydslPredicateExecutor, Querydsl Web 지원, 리포지토리 지원 - QuerydslRepositorySupport, Querydsl 지원 클래스 직접 만들기 본문
인프런 - 스프링부트와 JPA실무 로드맵/실전! Querydsl
스프링 데이터 JPA가 제공하는 Querydsl기능, 인터페이스 지원 - QuerydslPredicateExecutor, Querydsl Web 지원, 리포지토리 지원 - QuerydslRepositorySupport, Querydsl 지원 클래스 직접 만들기
소소한나구리 2024. 10. 31. 15:05출처 : 인프런 - 실전! Querydsl (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
** 참고
- 여기서 소개하는 기능은 제약이 커서 복잡한 실무 환경에서 사용하기에는 많이 부족함
- 스프링 데이터에서 제공하는 기능이기에 간단히 소개하고 왜 부족한지에 대해서 초점을 맞춰 설명
1. 인터페이스 지원 - QuerydslPredicateExecutor
1) 인터페이스 내부 구조
- 해당 인터페이스에 선언된 메서드들을 사용할 수 있으며 직접 들어가서 살펴보면 동일한 이름으로 오버로딩된 메서드들이 더 있음
- Pageable, Sort기능도 지원함
- 공식 문서
public interface QuerydslPredicateExecutor<T> {
Optional<T> findOne(Predicate predicate);
Iterable<T> findAll(Predicate predicate);
long count(Predicate predicate);
boolean exists(Predicate predicate);
<S extends T, R> R findBy(Predicate predicate, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);
// ... 등등
}
2) 예제 코드
(1) 리포지토리에 적용
- 스프링 데이터 JPA를 사용하는 리포지토리에 QuerydslPredicateExecutor<엔터티>를 상속
public interface MemberRepository extends JpaRepository<Member, Long>,
MemberRepositoryCustom,
QuerydslPredicateExecutor<Member> {
List<Member> findByUsername(String username);
}
(2) 테스트
- findAll메서드의 인자에 조건을 바로 입력하여 결과를 조회할 수 있으며 실행해보면 쿼리도, 반환값도 정상적으로 출력됨
- 단순히 메서드만 호출해서 인수로 검색 조건을 입력할 수 있어서 편하다고 느낄 수 있지만 한계가 분명함
// QuerydslPredicateExecutor Test
@Test
void querydslPredicateExecutorTest() {
// ... 초기화 코드 생략
Iterable<Member> result = memberRepository
.findAll(member.age.between(10, 30)
.and(member.username.eq("member1")));
for (Member member : result) {
System.out.println("member = " + member);
}
}
(3) 한계점
- 묵시적 조인은 가능하지만 left join이 불가능하기에 관계형 데이터베이스를 주로 사용하는 실무에서는 치명적인 단점일 수 있음
- 클라이언트, 서비스에서 Querydsl에 의존할 수밖에 없어 Querydsl을 다른기술로 바꾸게 되면 많은 코드가 바뀌어야함
- 특히 실무환경은 상당히 복잡하기 때문에 유연하지 않은 기술을 실무에서 사용하기에 쉽지 않음
2. Querydsl Web지원
1) 설명
(1) 공식문서 예제
- 공식 문서
- Controller의 파라미터로 Predicate를 받으면 쿼리 파라미터로 전송된 값들을 컨트롤러의 코드에서 리포지토리의 메서드를 호출할 때 인수의 값으로 입력하여 사용할 수 있음
@Controller
class UserController {
@Autowired UserRepository repository;
@RequestMapping(value = "/", method = RequestMethod.GET)
String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate,
Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {
model.addAttribute("users", repository.findAll(predicate, pageable));
return "index";
}
}
(2) 한계점
- equals, contains, in을 공식 지원하며 사실상 정상적으로 사용가능한 기능은 equals 정도만 원활하게 사용할 수 있으며, 실무에서 사용 가능한 경우가 거의 없음
- 조건을 커스텀하는 기능이 너무 복잡하고 명시적이지 않음
- 컨트롤러가 Querydsl에 의존하게되어 계층구조가 무너짐
- 마찬가지로 복잡한 실무환경에서는 사용하기가 한계가 너무 명확하기에 사용하지 말 것을 권장하며 실습도 생략
3. 리포지토리 지원 - QuerydslRepositorySupport
1) 설명
- 공식 메뉴얼에는 나와있지 않으며 특별한 기능을 제공함
- 추상클래스여서 상속받아서 해당 클래스에 선언된 기능들을 사용할 수 있으며 EntityManager도 자동으로 주입을 받고 Querydsl이라는 클래스가 선언되어있어 해당 기능들도 사용 가능함
2) 적용
(1) 사용자 정의 리포지토리에 상속
- 사용자 정의 리포지토리에 QuerydslRepositorySupport 클래스를 상속받고 생성자에서 super 연산자로 조상클래스에 엔터티를 넘겨주면 됨
- 생성자의 코드가 깔끔해짐
public class MemberRepositoryCustomImpl extends QuerydslRepositorySupport implements MemberRepositoryCustom {
public MemberRepositoryCustomImpl() {
super(Member.class);
}
// ... 기존코드 생략
}
(2) 기존 메서드를 수정하여 적용해보기
- 기존에 테스트로 작성했던 searchPageComplex메서드를 수정
- 기존에 offset, limit 정보를 쿼리에서 직접 작성해야 했던 것을 getQuerydsl().applyPagination()으로 페이지 정보와 쿼리를 입력하면 자동으로 offset과 limit 정보를 입력해서 반환해줌
- 반환된 결과를 fetch()로 쿼리의 결과를 반환하면 됨
- 특이한점은 쿼리가 from부터 시작하여 가독성이 좋지 않음(대부분의 개발자는 SQL 문법에 익숙하기 때문)
// QuerydslRepositorySupport를 사용하여 메서드를 작성
public Page<MemberTeamDto> searchQuerydslRepositorySupport(MemberSearchCondition condition, Pageable pageable) {
JPQLQuery<MemberTeamDto> querydsl = 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, querydsl);
List<MemberTeamDto> result = query.fetch();
return getPage(result, pageable,
() -> queryFactory
.select(member.count())
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
).fetchOne());
}
(3) 한계
- Querydsl 3.x 버전을 기반으로 동작하기에 from()부터 쿼리를 작성해야하며 Querydsl 4.x에 나온 JPAQueryFactory를 사용해야 select부터 시작할 수 있음
- 즉, 쿼리를 가독성을 좋게 하기 위하여 JPAQueryFactory를 사용하려면 생성자에서 EntityMaganer를 파라미터로 받아서 JPAQueryFactory의 객체를 생성해야 하는데, 생성자의 코드가 깔끔해졌던 장점이 사라지게됨
- 그리고 스프링 데이터의 Sort 기능이 버그로인하여 정상적으로 동작하지 않기에 페이징 기능만 사용할 수 있어 실무에서 사용하기에 불안요소가 존재함
// JPAQueryFactory를 사용하기위해서는 이렇게 생성자를 생성해야하는데, 오히려 더 지저분해짐
private final JPAQueryFactory queryFactory;
public MemberRepositoryCustomImpl(EntityManager em) {
super(Member.class);
queryFactory = new JPAQueryFactory(em);
}
4. Querydsl 지원 클래스 직접 만들기
** 참고
- QuerydslRepositorySupport가 지닌 한계를 극복하기 위해 직접 Querydsl 지원 클래스를 작성
- 직접 구현해보면서 라이브러리를 사용하지 않아도 유틸리티 클래스나 지원 클래스를 직접 만들어서 적용하는 과정을 보고 실제 프로젝트에서 어떻게 코드를 줄일 수 있을지에 대한 관점으로 해당 과정을 진행
1) 코드
(1) 구현 항목
- 스프링 데이터가 제공하는 페이징을 편리하게 변환
- 페이징과 카운트 쿼리 분리
- 스프링 데이터 Sort 지원
- select(), selectFrom()으로 시작가능
- EntityManager, QueryFactory제공
(2) 클래스 생성
- 추상클래스로 생성하여 Querydsl을 사용할 때 필요한 정보를 모두 집어넣은 집약체 클래스
- 강의에서는 applyPagination()에 Deprecated된 fetchCount()를 사용하여 전체 카운트 쿼리를 가져오도록 작성되어있어 하나는 삭제하고 하나는 countQuery를 fetchOne으로 실행되도록 수정함
- @Autowired로 주입하고있는 setentityManager()의 메서드에서 동적 Sort 버그가 발생하는 원인을 해결하는 코드가 들어있음
/**
* 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,
Function<JPAQueryFactory, JPAQuery> countQuery) {
JPAQuery jpaContentQuery = contentQuery.apply(getQueryFactory());
List<T> content = getQuerydsl().applyPagination(pageable, jpaContentQuery).fetch();
JPAQuery<Long> countResult = countQuery.apply(getQueryFactory());
return PageableExecutionUtils.getPage(content, pageable, countResult::fetchOne);
}
}
(3) 구현된 지원클래스를 사용
- Querydsl4RepositorySuppoert를 상속받는 클래스를 만들어서 해당 메소드들을 사용하면 됨
- 상속받은 Support클래스의 applyPagination와 동일한 기능을 하도록 직접 구현한 searchPageByApplyPage()와 applyPagination을 사용한 applyPagination()의 쿼리코드를 보면 직접 구현한 것보다 파라미터로 쿼리만 넘길 수 있도록 상속받을 메서드를 사용하는 것이 조금 더 가독성이 좋은 것을 볼 수 있음
- 이렇게 미리 가독성이 좋아보이게하거나, 코드를 줄일 수 있는 용도로 기존 라이브러리의 기능을 수정하거나 새롭게 정의하여 작성하여 실제 프로젝트에 사용할 수도 있음
package study.querydsl.repository;
@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,
() -> Optional.ofNullable(
select(member.count())
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
).fetchOne()).orElse(0L));
}
// 위와 동일한 코드, applyPagination()의 인수로 페이지 정보와 contentQuery, countQuery를 람다식으로 넘겨서 계산
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())),
countQuery -> countQuery
.select(member.count())
.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;
}
}
(4) Test
- 교재에는 Test가 없지만 직접 테스트 케이스를 만들어서 실행해보면 두개의 메서드가 모두 동일하게 정상 동작하고, 출력된 결과와 쿼리가 모두 동일하게 동작하는 것을 알 수 있음
// Querydsl4RepositorySupport 메소드 테스트
@Test
void applyPagination() {
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));
}
MemberSearchCondition condition = new MemberSearchCondition(); // 예제이므로 조건은 입력 X
condition.setAgeGoe(30);
condition.setAgeLoe(45);
PageRequest pageRequest = PageRequest.of(1, 5); // 페이지 정보만 생성
Page<Member> members1 = memberTestRepository.applyPagination(condition, pageRequest);
assertThat(members1.getSize()).isEqualTo(5);
int i = 0;
for (Member member : members1) {
System.out.println("member = " + member);
assertThat(member.getAge()).isEqualTo(35+i);
i++;
}
Page<Member> members2 = memberTestRepository.searchPageByApplyPage(condition, pageRequest);
for (Member member : members1) {
System.out.println("member = " + member);
}
}
/* 실행 결과 일부
applyPagination 실행 결과 로그
where member1.age >= 301 and member1.age <= 452 */ select count(m1_0.member_id) from member m1_0 where m1_0.age>=NULL and m1_0.age<=?;
member = Member(id=36, username=member35, age=35)
member = Member(id=37, username=member36, age=36)
member = Member(id=38, username=member37, age=37)
member = Member(id=39, username=member38, age=38)
member = Member(id=40, username=member39, age=39)
searchPageByApplyPage 실행 결과 로그
where member1.age >= 301 and member1.age <= 452 */ select count(m1_0.member_id) from member m1_0 where m1_0.age>=NULL and m1_0.age<=?;
member = Member(id=36, username=member35, age=35)
member = Member(id=37, username=member36, age=36)
member = Member(id=38, username=member37, age=37)
member = Member(id=39, username=member38, age=38)
member = Member(id=40, username=member39, age=39)
*/