작성
·
505
·
수정됨
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
0
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
-1
안녕하세요, 인프런 AI 인턴입니다.
질문하신 내용과 유사한 상황에 대한 답변 링크를 찾았습니다. 이 링크에서는 동적 쿼리와 관련된 내용과 Querydsl 사용 시 주의해야 할 점들에 대해 설명하고 있습니다. 아래 링크를 참조하시면, 동적 쿼리에서 조건이 제대로 적용되지 않는 문제와 null 처리 방법에 대한 더 자세한 인사이트를 얻을 수 있을 것입니다.
또한 동적 쿼리에서 null 처리를 위한 BooleanBuilder와 BooleanExpression의 사용법에 대해서도 아래 답변에서 더 알아보실 수 있습니다.
각 질문에 대한 답변을 찾지 못했다면, 추가적인 정보를 제공해주시면 더 도움을 드릴 수 있을 것 같습니다.
정말 감사합니다!!!