스프링/QueryDsl

[QueryDsl] SpringDataJPA (Interface)에 원하는 QueryDsl method 추가하기

nomoreFt 2022. 3. 22. 13:45

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) {
        //어쩌구 저쩌구 비즈니스 로직
    }