SpringDataJPA는 interface로 상속받아 구현하는데, QueryDsl 메서드를 추가하려면 구현체가 필요하다.
기본적인 SpringDataJPA를 상속받은 interface Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
//select m from Member m where m.username = ?
List<Member> findByUsername(String username);//naming 규칙으로 추가한 method
}
그렇다면, 이 SpringDataJPA interface에 queryDsl 메서드를 추가하려면 어떻게?
🎁interface는 상속이 무한이라는 점을 이용한다.
방법 1. SpringDataJPA interface에 사용자 정의 Repository Interface를 만들고, 그 구현체에서 구현
- SpringDataJPA Repository(Interface) -> 내가 만든 Custom interface + JpaRepository 상속
- SpringDataJPA RepositoryImpl(Class) <- Custom interface에 선언된 메서드도 구현한다. (이때 구현을 querydsl로)
CustomInterface를 만든다. (SpringDataJpa Interface에 상속시켜 구현할 선언 메서드 집합)
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
SpringDataJpa가 custom interface를 상속한다.
public interface MemberRepository extends JpaRepository<Member, Long> , ****MemberRepositoryCustom**** {
//select m from Member m where m.username = ?
List<Member> findByUsername(String username);
}
SpringDataJPA를 구현하여 구현체를 생성 (implements)
MemberRepositoryCustom에 있는 search, searchPageSimple, searchPageComplex를 구현한 모습
- 🎁 구현체는 naming을 지켜줘야한다. (SpringDataJpa interface 네임 + Impl)
- ex )
MemberRepository
+Impl
=MemberRepositoryImpl
- custom과는 구현체 네이밍과 상관 없으니 주의🥊
public class MemberRepositoryImpl implements MemberRepositoryCustom{
@Autowired
private JPAQueryFactory queryFactory;
@Override
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return queryFactory
.select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName")))
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
).fetch();
}
@Override
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
QueryResults<MemberTeamDto> result = 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();//fetch가 아닌 fetchResult를 써야 count, page 쿼리 두개를 날린다.
List<MemberTeamDto> content = result.getResults();
long total = result.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(); //fetch를 해서 content만 가져온다.
/* long count = queryFactory
//.select(Wildcard.count) //select count(*)
.select(member.count())
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
).fetchOne();*/
//분리의 장점
//CONTENT는 복잡한데, COUNT는 간단할 수 있다. (JOIN을 줄이거나, WHERE절이 적어도 상관 없거나
//QeuryDsl에서 제공하는 fetchResult를 쓰면
//같은 query로 count를 구해오기 때문에, 위와 같은 경우에는
//따로 pageCount를 count 하는게 좋다.
JPAQuery<Long> countQuery = queryFactory
.select(member.count())
.from(member)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
);
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
}
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;
}
private BooleanExpression ageBetween(int ageLoe, int ageGoe) {
return ageGoe(ageGoe).and(ageGoe(ageGoe));
}
}
방법 2. dto등 화면과 fit하다면 그냥 상속시키지말고 따로 Repository 하나 만드는 것도?
너무 SpringDataJpa에 상속시켜야 한다는 강박은 있을 필요 없다.
@Repository
public class MemberQueryReposiroty {
@Autowired
private JPAQueryFactory queryFactory;
public List<MemberTeamDto> search(MemberSearchCondition condition) {
//어쩌구 저쩌구 비즈니스 로직
}