인프런 커뮤니티 질문&답변

유요한님의 프로필 이미지
유요한

작성한 질문수

실전! Querydsl

동적 쿼리가 안되는 이유

작성

·

497

·

수정됨

0


@Repository
@Log4j2
public abstract class Querydsl4RepositorySupport {
    // 이 클래스가 다루는 도메인(엔터티)의 클래스
    private final Class domainClass;
    // 도메인 엔터티에 대한 Querydsl 쿼리를 생성하고 실행
    private Querydsl querydsl;
    // 데이터베이스와의 상호 작용을 담당하는 JPA의 핵심 객체
    private EntityManager entityManager;
    // queryFactory를 통해 Querydsl 쿼리를 생성하고 실행합니다.
    private JPAQueryFactory queryFactory;

    public Querydsl4RepositorySupport(Class<?> domainClass) {
        Assert.notNull(domainClass, "Domain class must not be null!");
        this.domainClass = domainClass;
    }

    // Pageable안에 있는 Sort를 사용할 수 있도록 설정한 부분
    @Autowired
    public void setEntityManager(EntityManager entityManager) {
        Assert.notNull(entityManager, "EntityManager must not be null!");
        // JpaEntityInformation을 얻기 위해 JpaEntityInformationSupport를 사용합니다.
        // 이 정보는 JPA 엔터티에 대한 메타데이터 및 정보를 제공합니다.
        JpaEntityInformation entityInformation =
                JpaEntityInformationSupport.getEntityInformation(domainClass, entityManager);
        // 이는 Querydsl에서 엔터티의 경로를 생성하는 데 사용됩니다.
        SimpleEntityPathResolver resolver = SimpleEntityPathResolver.INSTANCE;
        // entityInformation을 기반으로 엔티티의 경로를 생성합니다.
        EntityPath path = resolver.createPath(entityInformation.getJavaType());
        this.entityManager = entityManager;
        // querydsl 객체를 생성합니다.
        // 이 객체는 Querydsl의 핵심 기능을 사용할 수 있도록 도와줍니다.
        // 엔터티의 메타모델 정보를 이용하여 Querydsl의 PathBuilder를 생성하고, 이를 이용하여 Querydsl 객체를 초기화합니다.
        this.querydsl = new Querydsl(entityManager, new
                PathBuilder<>(path.getType(), path.getMetadata()));
        this.queryFactory = new JPAQueryFactory(entityManager);
    }

    // 해당 클래스의 빈(Bean)이 초기화될 때 자동으로 실행되는 메서드
    @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!");
    }
    // 이 팩토리는 JPA 쿼리를 생성하는 데 사용됩니다.
    protected JPAQueryFactory getQueryFactory() {
        return queryFactory;
    }
    // 이 객체는 Querydsl의 핵심 기능을 사용하는 데 도움이 됩니다.
    protected Querydsl getQuerydsl() {
        return querydsl;
    }
    // EntityManager는 JPA 엔터티를 관리하고 JPA 쿼리를 실행하는 데 사용됩니다.
    protected EntityManager getEntityManager() {
        return entityManager;
    }
    // Querydsl을 사용하여 쿼리의 SELECT 절을 생성하는 메서드입니다.
    // expr은 선택할 엔터티나 엔터티의 속성에 대한 표현식입니다.
    protected <T> JPAQuery<T> select(Expression<T> expr) {
        return getQueryFactory().select(expr);
    }
    // Querydsl을 사용하여 쿼리의 FROM 절을 생성하는 메서드입니다.
    // from은 엔터티에 대한 경로 표현식입니다.
    protected <T> JPAQuery<T> selectFrom(EntityPath<T> from) {
        return getQueryFactory().selectFrom(from);
    }

    // 이 메서드는 주어진 contentQuery를 사용하여 Querydsl을 통해 JPA 쿼리를 생성하고 실행하고,
    // 그 결과를 Spring Data의 Page 객체로 변환하는 기능을 제공
    protected <T> Page<T> applyPagination(Pageable pageable,
                                          Function<JPAQueryFactory, JPAQuery> contentQuery) {
        // 1. contentQuery를 사용하여 JPAQuery 객체를 생성
        JPAQuery jpaQuery = contentQuery.apply(getQueryFactory());
        // 2. Querydsl을 사용하여 페이징 및 정렬된 결과를 가져옴
        List<T> content = getQuerydsl().applyPagination(pageable,
                jpaQuery).fetch();
        // 3. contentQuery를 다시 사용하여 countQuery를 생성
        JPAQuery<Long> countQuery = contentQuery.apply(getQueryFactory());
        // 4. countQuery를 실행하고 총 레코드 수를 얻음
        long total = countQuery.fetchOne();
        // 5. content와 pageable 정보를 사용하여 Spring Data의 Page 객체를 생성하고 반환
        return PageableExecutionUtils.getPage(content, pageable,
                () -> total);
    }
    // 이 메서드는 contentQuery와 함께 countQuery를 인자로 받아서 사용합니다.
    // contentQuery를 사용하여 페이징된 결과를 가져오고, countQuery를 사용하여 전체 레코드 수를 얻습니다.
    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());
        log.info("countResult : " + countResult );

        Long total = countResult.fetchOne();
        return PageableExecutionUtils.getPage(content, pageable,
                () -> total);
    }
}
 // count처리 까지 한것
    public Page<Member> applyPagination2(MemberSearchCondition condition, Pageable pageable) {
        return applyPagination(pageable, contentQuery ->
                contentQuery.selectFrom(member)
                        .join(member.team, team).fetchJoin()
                        .where(userNameEq(condition.getUserName()),
                                teamNameEq(condition.getTeamName()),
                                ageGoe(condition.getAgeGoe()),
                                ageLoe(condition.getAgeLoe())
                        ), countQuery -> countQuery
                .select(member.count())
                .from(member)
                .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;
    }

서비스에서

    public Page<MemberTeamDTO> search(MemberSearchCondition condition, Pageable pageable) {

        Page<Member> resultPage = memberTestRepository.applyPagination2(condition, pageable);
        return resultPage.map(member -> MemberTeamDTO.builder()
                .memberId(member.getId())
                .age(member.getAge())
                .userName(member.getUserName())
                .teamId(member.getTeam().getId())
                .teamName(member.getTeam().getName())
                .build());
    }

컨트롤러

    @GetMapping("/v3/members")
    public ResponseEntity<?> searchMemberV3(MemberSearchCondition condition,
                                            Pageable pageable) {
        Page<MemberTeamDTO> search = memberService.search(condition, pageable);
        return ResponseEntity.ok().body(search);
    }

이렇게 작성했는데 뭐가 문제인지 페이지와 정렬은 잘되는데 http://localhost:9090/v3/members?teamName=teamA&page=1&sort=id,desc

 

teamA와 teamB 모두 나오고 있습니다.

질문1 : 조건이 안 먹고 있는데 왜 그럴까요? 어떤 조건으로 해도 안 먹고 있습니다.

질문2 : 여기서 동적 쿼리에서 null 처리를 잘 해야 하는 이유가 null로 들어오면 무시되서 전부 조회가 되기 때문에 null 처리를 잘해야하는 거 맞나요?

질문 3 : 예를들어, hasText(userName) ? member.userName.eq(userName) : null; 가 있으면 조건이 userName이 존재한다고 하면 true이지만 받아온 userName이 존재하지 않는 userName이면 null이 반환이 되나요?

 

질문 4 : null 문제 해결 글을 찾던 와중에

@DataJpaTest
public class DynamicQueryTest {

    JPAQueryFactory queryFactory;
    @Autowired
    EntityManager em;

    @BeforeEach
    void init() {
        queryFactory = new JPAQueryFactory(em);

        em.persist(new Member("userA", 10, "ROLE_MASTER"));
        em.persist(new Member("userB", 20, "ROLE_ADMIN"));
        em.persist(new Member("userC", 30, "ROLE_USER"));
    }

    @Test
    void dynamicQuery() {

//        Integer age = 10;
//        String role = "ROLE_MASTER";
        Integer age = null;
        String role = null;

        List<Member> result = queryFactory
                .selectFrom(member)
                .where(ageAndRoleEq(age, role))
                .fetch();

        System.out.println("result = " + result);

    }

    private BooleanBuilder ageAndRoleEq(Integer age, String role) {
        return ageEq(age).and(roleEq(role));
    }

    private BooleanBuilder ageEq(Integer age) {
        if (age == null) {
            return new BooleanBuilder();
        } else {
            return new BooleanBuilder(member.age.eq(age));
        }
    }

    private BooleanBuilder roleEq(String roleName) {
        if (roleName == null) {
            return new BooleanBuilder();
        }
        return new BooleanBuilder(member.roleName.eq(roleName));
    }

}
    return nullSafeBuilder(() -> member.age.eq(age));
}

private BooleanBuilder roleEq(String roleName) {
    return nullSafeBuilder(() -> member.roleName.eq(roleName));
}

public static BooleanBuilder nullSafeBuilder(Supplier<BooleanExpression> f) {
    try {
        return new BooleanBuilder(f.get());
    } catch (IllegalArgumentException e) {
        return new BooleanBuilder();
    }
}

이렇게 줄일 수 있다고 나왔는데 수업에서는 BooleanExpression을 사용했지만 null 방지를 위해서 BooleanBuilder 을 사용하는건가요? 이렇게 하면 null 방지가 되면서 전체 조회가 안되는 형식인가요?

 

답변 4

2

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. 요한님

현재 코드에는 2가지 문제가 있습니다.

 

1. MemberSearchCondition

컨트롤러에서 웹의 값을 바인딩 받을 때 안되는 문제

- 빈 생성자가 있는 경우 Setter가 있어야 값이 설정됩니다.

- 빈 생성자가 없다면 Setter를 생략하고 생성자 만으로 값을 주입할 수 있습니다.

지금 코드는 다음과 같습니다.

@ToString
@Getter
@NoArgsConstructor
public class MemberSearchCondition {
    // 회원명, 팀명, 나이(ageGoe, ageLoe)
}

이 코드는 @NoArgsConstructor를 사용해서 빈 생성자를 가지고 있습니다. 따라서 @Setter를 넣어주어야 합니다.

또는 @NoArgsConstructor를 제거하면 됩니다.

 

2. MemberSearchCondition.Sort는 내부에 DESC, ASC라는 값만 받을 수 있습니다. 이 필드를 제거해주세요. 또는 다음과 같이 sort에서 받을 수 있는 값만 받아야 합니다.

http://localhost:9090/v3/members?teamName=teamA&page=1&sort=DESC

 

==추가로 질문주신 부분들에 대해서 답을 드릴게요.==

질문1 : 조건이 안 먹고 있는데 왜 그럴까요? 어떤 조건으로 해도 안 먹고 있습니다.

-> 앞서 말씀드린 내용을 참고해주세요.

 

질문2 : 여기서 동적 쿼리에서 null 처리를 잘 해야 하는 이유가 null로 들어오면 무시되서 전부 조회가 되기 때문에 null 처리를 잘해야하는 거 맞나요?

-> 네 맞습니다.

 

질문 3 : 예를들어, hasText(userName) ? member.userName.eq(userName) : null; 가 있으면 조건이 userName이 존재한다고 하면 true이지만 받아온 userName이 존재하지 않는 userName이면 null이 반환이 되나요?

-> 네 맞습니다. 자바의 삼항 연산자를 검색해보시면 도움이 되실거에요. where에서 null이 반환되면 조건을 사용하지 않습니다.

 

질문 4 : null 문제 해결 글을 찾던 와중에...

-> 아닙니다. 이렇게 해도 모든 조건이 null이면 전체 조회가 됩니다. 이 방법은 다음과 같은 문제를 해결하기 위해 존재합니다.

다음 코드는 문제가 될 수 있는 코드입니다.

    private BooleanBuilder ageAndRoleEq(Integer age, String role) {
        return ageEq(age).and(roleEq(role));
    }

    private BooleanBuilder ageEq(Integer age) {
        if (age != null) {
            return member.age.eq(age)
        } else {
            return null;
        }
    }

    private BooleanBuilder roleEq(String roleName) {
        if (roleName != null) {
            return member.roleName.eq(roleName)
        } else {
            return null;
        }
    }

 

이 코드의 경우 첫번째 조건인 ageEq(age)에서 age가 null이라면 다음과 같이 해석됩니다.

ageEq(age).and(roleEq(role)) // 1. 원래 코드

null.and(roleEq(role)) //ageEq(age) // 2. 실행 후 코드

ageEq(age)의 결과가 null이기 때문에 이후에 null. 을 찍게 되면서 NullPointerException이 발생하게 됩니다.

질문 4에서 적어주신 코드는 ageEq(age)의 결과가 null이 아니라 빈 BooleanBuilder()이기 때문에 NullPointerException이 발생하지 않습니다.

도움이 되셨길 바래요 :)

유요한님의 프로필 이미지
유요한
질문자

정말 감사합니다!!!

0

김영한님의 프로필 이미지
김영한
지식공유자

추가로 수강평에 남겨주셨던 AI 인턴 관련 이슈도 인프런과 이야기 나누고 인프런에서 조치해두었습니다.

관련해서 글 남겨두었으니 참고해주세요

https://inf.run/MjAS1

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. 유요한님

도움을 드리고 싶지만 질문 내용만으로는 답변을 드리기 어렵습니다. 코드를 저희가 돌려봐야 정확한 답변을 드릴 수 있을 것 같아요.

실제 동작하는 전체 프로젝트를 압축해서 구글 드라이브로 공유해서 링크를 남겨주세요.

구글 드라이브 업로드 방법은 다음을 참고해주세요.

https://bit.ly/3fX6ygx


주의: 업로드시 링크에 있는 권한 문제 꼭 확인해주세요

 

추가로 다음 내용도 코멘트 부탁드립니다.

1. 문제 영역을 실행할 수 있는 방법

2. 문제가 어떻게 나타나는지에 대한 상세한 설명

 

링크: 공식 서포터즈

링크: 자주하는 질문

감사합니다.

유요한님의 프로필 이미지
유요한
질문자

https://drive.google.com/file/d/1W9rPWXQ3HU-NN0icWiQANSIZKSeepY6y/view?usp=sharing

공유 링크이고

호스트를 9090으로 바꾸어서 http://localhost:9090/v3/members?teamName=teamA&page=1&sort=id,desc

이렇게 했는데 안됩니다. 근데 http://localhost:9090/v3/members?page=1&sort=id,desc
이렇게 페이지와 정렬은 잘됩니다. 즉, 동적쿼리에서 where 조건에서 안되는거로 확인이 되는데
v3으로 하면 Querydsl 지원클래스 만들기에서 나오는 방법으로 진행한거고 v5는 OrderSpecifier을 사용한 방법인데 둘 다 안되었습니다. 컨트롤러에서 로그를 찍어본 결과 파라미터로 받은 것이 수업에서 나온 방법인 객체에 담아지지 않는거 같습니다.

 

로그 결과 : condition : MemberSearchCondition(userName=null, teamName=null, ageGoe=null, ageLoe=null, orderBy=null, sort=null)

 

teamName=teamA로 해도 null로 찍혔습니다. 이게 위에서 질문드린 질문1번에 해당되는 문제였습니다.

질문2는 동적 쿼리에서 null 처리를 잘 해야 하는 이유가 null로 들어오면 무시되서 전부 조회가 되기 때문에 null 처리를 잘해야하는 거 맞나요?

 

질문3은 hasText(userName) ? member.userName.eq(userName) : null; 가 있으면 조건이 userName이 존재한다고 하면 true이지만 받아온 userName이 존재하지 않는 userName이면 null이 반환이 되나요?

 

질문4 null 문제 해결 글을 찾던 와중에

@DataJpaTest
public class DynamicQueryTest {

    JPAQueryFactory queryFactory;
    @Autowired
    EntityManager em;

    @BeforeEach
    void init() {
        queryFactory = new JPAQueryFactory(em);

        em.persist(new Member("userA", 10, "ROLE_MASTER"));
        em.persist(new Member("userB", 20, "ROLE_ADMIN"));
        em.persist(new Member("userC", 30, "ROLE_USER"));
    }

    @Test
    void dynamicQuery() {

//        Integer age = 10;
//        String role = "ROLE_MASTER";
        Integer age = null;
        String role = null;

        List<Member> result = queryFactory
                .selectFrom(member)
                .where(ageAndRoleEq(age, role))
                .fetch();

        System.out.println("result = " + result);

    }

    private BooleanBuilder ageAndRoleEq(Integer age, String role) {
        return ageEq(age).and(roleEq(role));
    }

    private BooleanBuilder ageEq(Integer age) {
        if (age == null) {
            return new BooleanBuilder();
        } else {
            return new BooleanBuilder(member.age.eq(age));
        }
    }

    private BooleanBuilder roleEq(String roleName) {
        if (roleName == null) {
            return new BooleanBuilder();
        }
        return new BooleanBuilder(member.roleName.eq(roleName));
    }

}
    return nullSafeBuilder(() -> member.age.eq(age));
}

private BooleanBuilder roleEq(String roleName) {
    return nullSafeBuilder(() -> member.roleName.eq(roleName));
}

public static BooleanBuilder nullSafeBuilder(Supplier<BooleanExpression> f) {
    try {
        return new BooleanBuilder(f.get());
    } catch (IllegalArgumentException e) {
        return new BooleanBuilder();
    }
}

이렇게 줄일 수 있다고 나왔는데 수업에서는 BooleanExpression을 사용했지만 조합 시 and나 or 같은거는 null and 값 이런식으로 되면 null 에러가 발생하니 null에러 방지를 위해서 BooleanBuilder 을 사용하는건가요? 이렇게 하면 null 방지가 되면서 전체 조회가 안되는 형식인가요?

 

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. 요한님

올려주신 파일이 잘못된 것 같습니다.

압축을 풀어보니 파일은 나오는데, 파일 내부에 인코딩이 모두 깨져서 확인이 되지 않습니다.

MAC, 윈도우 모두 확인해보았는데 같은 문제가 발생합니다.

다시 확인 부탁드려요.

=== 추가 확인 내용 ===

확인해보니 build.gradle, settings.gradle에 인코딩이 깨지네요.

이 부분을 제외하고는 문제가 없습니다. 파일은 따로 안올려주셔도 됩니다 :)

유요한님의 프로필 이미지
유요한
질문자

혹시 또 깨질 것을 대비해서 git 주소도 보냅니다.(원래 Q타입은 올라가면 안된다고 하지만 인코딩이 또깨지면 볼 수 없으니 git에서 해결할 수 있게 올렸습니다)

https://github.com/YuYoHan/querydsl
https://drive.google.com/drive/folders/1LQbHKzewHaHALbJq3NFLC79SsrnQPgRG?usp=sharing

 

김영한님의 프로필 이미지
김영한
지식공유자

요한님 코드는 확인 완료했습니다.

저작권 때문에 Git에 전체 코드를 오픈하면 안됩니다 🙂

git 코드는 private으로 변경해주세요.

-1

안녕하세요, 인프런 AI 인턴입니다.

질문하신 내용과 유사한 상황에 대한 답변 링크를 찾았습니다. 이 링크에서는 동적 쿼리와 관련된 내용과 Querydsl 사용 시 주의해야 할 점들에 대해 설명하고 있습니다. 아래 링크를 참조하시면, 동적 쿼리에서 조건이 제대로 적용되지 않는 문제와 null 처리 방법에 대한 더 자세한 인사이트를 얻을 수 있을 것입니다.

또한 동적 쿼리에서 null 처리를 위한 BooleanBuilder와 BooleanExpression의 사용법에 대해서도 아래 답변에서 더 알아보실 수 있습니다.

각 질문에 대한 답변을 찾지 못했다면, 추가적인 정보를 제공해주시면 더 도움을 드릴 수 있을 것 같습니다.

유요한님의 프로필 이미지
유요한

작성한 질문수

질문하기