쿼리DSL 연관관계 JOIN하여 DTO로 조회하기
안녕하세요 영한님의 강의를 맹신하는 대학생 개발자입니다!
다름이 아니라 플랫폼을 개발하고 있는데 연관관계가 포함된 People과 PeopleProfile 사이에서 join을 통해 값을 가져오려고 합니다.
PeopleEntity
@Getter
@Entity
@Table(name = "people")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class People extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "people_id")
private Long peopleId;
@Column(name = "nickname")
private String nickname;
@Column(name = "email")
private String email;
@Column(name = "phone_number")
private String phoneNumber;
@Enumerated(EnumType.STRING)
@Column(name = "people_type")
private PeopleType peopleType;
@Column(name = "profile_image_uri")
private String profileImageUri;
@Column(name = "account_number")
private String accountNumber;
@OneToOne(cascade = CascadeType.REMOVE)
private Member member;
@OneToOne(mappedBy = "people")
private PeopleProfile peopleProfile;
...
}
PeopleProfile
@Entity
@Getter
@Table(name = "people_profile")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PeopleProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "people_profile_id")
private Long peopleProfileId;
@Column(name = "introduction")
private String introduction;
@Column(name = "activity_area")
private String activityArea;
@Column(name = "education")
private String education;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "people_id")
private People people;
@ElementCollection
@CollectionTable(name = "people_hashtags", joinColumns = @JoinColumn(name = "people_profile_id"))
@Column(name = "hashtags_id")
private List<Hashtag> hashtags = new ArrayList<>();
...
}
Hashtag
@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Hashtag {
@Column(name = "hashtag")
private String value;
private Hashtag(String value) {
this.value = validate(value);
}
private String validate(String value) {
return value;
}
public static Hashtag from(String value) {
return new Hashtag(value);
}
}hashtag는 값 타입으로 구현했습니다.
PeopleDTO
@Getter
public class CardPeopleResponse {
@NotNull
private Long peopleId;
@NotNull
private String nickname;
@NotNull
private String peopleType;
@NotNull
private String profileImageUri;
@Valid
private CardPeopleProfileResponse profile;
@QueryProjection
public CardPeopleResponse(final Long peopleId, final String nickname, final People Type peopleType,
final String profileImageUri, @Valid CardPeopleProfileResponse profile) {
this.peopleId = peopleId;
this.nickname = nickname;
this.peopleType = peopleType.name();
this.profileImageUri = profileImageUri;
this.profile = profile;
}
}PeopleProfileDTO
@Getter
public class CardPeopleProfileResponse {
@NotNull
private final String activityArea;
@NotNull
private final List<String> hashtags;
@NotNull
private final Integer completedProjectsCount;
@NotNull
private final Integer interestsCount;
@QueryProjection
public CardPeopleProfileResponse(final String activityArea, final List<Hashtag> hashtags,
final Integer completedProjectsCount, final Integer interestsCount) {
this.activityArea = activityArea;
this.hashtags = hashtags.stream().map(Hashtag::getValue).toList();
this.completedProjectsCount = completedProjectsCount;
this.interestsCount = interestsCount;
}
}
쿼리DSL
private List<CardPeopleResponse> getCardPeopleContent(Pageable pageable) {
return queryFactory
.select(new QCardPeopleResponse(
people.peopleId,
people.nickname,
people.peopleType,
people.profileImageUri,
new QCardPeopleProfileResponse(
peopleProfile.activityArea,
peopleProfile.hashtags,
Expressions.constant(0),
Expressions.constant(0)
)
))
.from(people)
.join(people.peopleProfile, peopleProfile)
.orderBy(getPeopleOrderSpecifiers(pageable.getSort()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
}
completedProjectsCount와 interestsCount는 추후 개발 예정이므로 Expressions.constant(0)으로 대체 해뒀습니다. 위와 같이 서버를 구현하였을때, 서버 구동은 문제 없이 됩니다.
문제점
2024-02-22T16:44:50.163+09:00 DEBUG 3487851 --- [io-52003-exec-1] org.hibernate.SQL :
select
p1_0.people_id,
p1_0.nickname,
p1_0.people_type,
p1_0.profile_image_uri,
p2_0.activity_area,
h1_0.hashtag
from
people p1_0
join
people_profile p2_0
on p1_0.people_id=p2_0.people_id
join
people_hashtags h1_0
on p2_0.people_profile_id=h1_0.people_profile_id
order by
p1_0.people_id desc offset ? rows fetch first ? rows only
2024-02-22T16:44:50.274+09:00 ERROR 3487851 --- [io-52003-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.InvalidDataAccessApiUsageException: argument type mismatch] with root cause
java.lang.IllegalArgumentException: argument type mismatch
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:na]
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499) ~[na:na]
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480) ~[na:na]
at com.querydsl.core.types.ConstructorExpression.newInstance(ConstructorExpression.java:144) ~[querydsl-core-5.0.0.jar!/:na]
at com.querydsl.core.types.FactoryExpressionUtils.compress(FactoryExpressionUtils.java:155) ~[querydsl-core-5.0.0.jar!/:na]
at com.querydsl.core.types.FactoryExpressionUtils.access$100(FactoryExpressionUtils.java:28) ~[querydsl-core-5.0.0.jar!/:na]
at com.querydsl.core.types.FactoryExpressionUtils$FactoryExpressionAdapter.newInstance(FactoryExpressionUtils.java:62) ~[querydsl-core-5.0.0.jar!/:na]
at com.querydsl.core.support.ConstantHidingExpression.newInstance(ConstantHidingExpression.java:87) ~[querydsl-core-5.0.0.jar!/:na]
...다음과 같은 argument type mismatch에러가 발생합니다. DTO에서 값을 받아와서 생성자에서 데이터 타입을 변경하면 문제가 되는걸까요? 이러한 경우에는 보통 어떻게 조회하는지 궁금합니다!
답변 1
1
안녕하세요, 인프런 AI 인턴입니다.
KeepHun님, 영한님의 강의에 대한 열심히 학습하는 자세가 인상적입니다.
DTO 생성자에서 발생하는 argument type mismatch 오류는 생성자로 전달된 인자의 타입이 기대하는 타입과 일치하지 않을 때 발생합니다. 이 경우, CardPeopleProfileResponse DTO의 생성자를 살펴봐야 할 것 같습니다.
DTO의 생성자는 다음과 같습니다:
public CardPeopleProfileResponse(final String activityArea, final List<Hashtag> hashtags,
final Integer completedProjectsCount, final Integer interestsCount) {
this.activityArea = activityArea;
this.hashtags = hashtags.stream().map(Hashtag::getValue).toList();
this.completedProjectsCount = completedProjectsCount;
this.interestsCount = interestsCount;
}
여기서 문제가 되는 부분은 List<Hashtag> 타입의 hashtags 입니다. 쿼리 결과에서는 해시태그 목록이 아닌 단일 Hashtag 객체를 생성자로 전달받으려 하고 있습니다. 즉, 생성자에 전달되어야 하는 것은 List<Hashtag> 타입인데, 실제 전달되는 것은 Hashtag 타입의 객체이기 때문에 오류가 발생합니다.
해결 방법으로는 select 문을 수정하여 List<Hashtag> 타입을 제대로 전달하는 방법을 사용해야 합니다.
예를 들어, 쿼리에서 해시태그 목록을 가져오기 위해 서브쿼리를 사용하거나, groupBy를 사용하여 hashtags를 모아서 반환해야 할 수도 있습니다. 이 부분은 QueryDSL의 구체적인 쿼리 구성에 따라 달라지므로, 아래와 같이 조정할 수 있습니다:
.select(new QCardPeopleResponse(
people.peopleId,
people.nickname,
people.peopleType,
people.profileImageUri,
new QCardPeopleProfileResponse(
peopleProfile.activityArea,
JPAExpressions.select(peopleProfile.hashtags).from(peopleProfile).where(peopleProfile.people.eq(people)),
Expressions.constant(0),
Expressions.constant(0)
)
))
또한, 쿼리에서 반환하는 값과 DTO 생성자의 파라미터 타입이 일치하는지 확인하시는 것이 중요합니다. Spring Data JPA에서 Projections를 사용하여 DTO로 결과를 매핑할 때에는 생성자의 파라미터 타입과 일치하는 필드만 갖고 오도록 할 수 있습니다. 위 정보에서 제공된 Spring Data JPA의 “Class-based Projections (DTOs)” 부분을 참조하여 각 필드가 일치하도록 쿼리 메서드를 작성하시면 이러한 에러를 피하실 수 있습니다.
다음은 Spring Data JPA의 “Projections” 부분에 나온 Class-based Projections에 대한 관련 내용입니다:
Class-based projections with JPQL is limited to constructor expressions in your JPQL expression, e.g. `SELECT new com.example.NamesOnly(u.firstname, u.lastname) from User u`. (Note the usage of a FQDN for the DTO type!) This JPQL expression can be used in `@Query` annotations as well where you define any named queries.
원문과 링크 정보는 아래와 같습니다.
- 원문: Class-based projections with JPQL is limited to constructor expressions in your JPQL expression, e.g.
SELECT new com.example.NamesOnly(u.firstname, u.lastname) from User u. (Note the usage of a FQDN for the DTO type!) This JPQL expression can be used in@Queryannotations as well where you define any named queries.
(출처: Spring Data JPA - Reference Documentation v3.1.5)
- 링크: Spring Data JPA - Projections
이렇게 클래스 기반의 DTO 프로젝션을 사용할 때는 JPQL 내의 constructor expressions로 제한되며, @Query 어노테이션을 사용하여 명시적인 쿼리를 정의할 수 있습니다.
KeepHun님께서는 쿼리 결과와 DTO 생성자 간의 타입 불일치를 해결하기 위한 위 정보를 참고하시어 문제를 해결하실 수 있으시길 바랍니다.
1
답변 코드 적용
private List<CardPeopleResponse> getCardPeopleContent(Pageable pageable) {
return queryFactory
.select(new QCardPeopleResponse(
people.peopleId,
people.nickname,
people.peopleType,
people.profileImageUri,
new QCardPeopleProfileResponse(
peopleProfile.activityArea,
JPAExpressions.select(peopleProfile.hashtags).from(peopleProfile).where(peopleProfile.people.eq(people))
)
))
.from(people)
.join(people.peopleProfile, peopleProfile)
.orderBy(getPeopleOrderSpecifiers(pageable.getSort()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
}
위와 같이 작성한 후 실행해보았습니다.
2024-02-25T23:08:06.122+09:00 ERROR 3847208 --- [io-52003-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: more than one row returned by a subquery used as an expression
Sql에 해당하는 객체가 하나의 반환값이 아닌 여러 개이기 때문에 에러가 발생하였습니다.
튜플로 조회
private List<CardPeopleResponse> getCardPeopleContent(Pageable pageable) {
List<Tuple> result = queryFactory
.select(
people.peopleId,
people.nickname,
people.peopleType,
people.profileImageUri,
peopleProfile.activityArea
peopleProfile.hashtags
)
.from(people)
.join(people.peopleProfile, peopleProfile)
.orderBy(getPeopleOrderSpecifiers(pageable.getSort()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
제가 알기로 객체가 여러 개인 상황에서는 Tuple로 반환받아야 한다고 알고 있습니다. 그래서 튜플로 코드를 처리하였습니다.
select
p1_0.people_id,
p1_0.nickname,
p1_0.people_type,
p1_0.profile_image_uri,
p2_0.activity_area,
h1_0.hashtag
from
people p1_0
join
people_profile p2_0
on p1_0.people_id=p2_0.people_id
join
people_hashtags h1_0
on p2_0.people_profile_id=h1_0.people_profile_id
order by
p1_0.people_id desc offset ? rows fetch first ? rows only
여전히 hashtags는 하나의 값만 조회했습니다.
최선의 해결책
private List<Hashtag> getHashtagsForPeople(Long peopleId) {
String sql = "SELECT h.hashtag FROM people_hashtags h WHERE h.people_profile_id = :peopleId";
List<String> hashtagValues = em.createNativeQuery(sql)
.setParameter("peopleId", peopleId)
.getResultList();
return hashtagValues.stream()
.map(Hashtag::from)
.collect(Collectors.toList());
}
private List<CardPeopleResponse> getCardPeopleContent(Pageable pageable) {
List<Tuple> result = queryFactory
.select(
people.peopleId,
people.nickname,
people.peopleType,
people.profileImageUri,
peopleProfile.activityArea
)
.from(people)
.join(people.peopleProfile, peopleProfile)
.orderBy(getPeopleOrderSpecifiers(pageable.getSort()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
return result.stream().map(tuple -> {
Long peopleId = tuple.get(people.peopleId);
String nickname = tuple.get(people.nickname);
PeopleType peopleType = tuple.get(people.peopleType);
String profileImageUri = tuple.get(people.profileImageUri);
String activityArea = tuple.get(peopleProfile.activityArea);
List<Hashtag> hashtags = getHashtagsForPeople(peopleId);
CardPeopleProfileResponse profile = new CardPeopleProfileResponse(activityArea, hashtags);
return new CardPeopleResponse(peopleId, nickname, peopleType, profileImageUri, profile);
}).toList();
}
따라서 저는 쿼리를 분석해 본 결과 JPA를 사용해서 모든 객체를 조회하는 경우가 아니라면 컬렉션 테이블을 조회할 수 없다는 것을 알게 되었습니다. 따라서 JPQL이 아닌 네이티브 쿼리를 사용해 구현에 성공하였습니다. 하지만 구현은 성공하였지만 아직까지 풀리지 않은 의문증이 있습니다.
컬렉션 테이블에 값은 수정 불가합니다. 그렇다면
OneToMany로 구현하고 수정 불가하게 구현하는 것과 다른 점이 무엇인가?왜
QueryDSL은 튜플에 리스트 객체를 못 담는가?
라는 의문이 들었습니다. 일단 인프런 AI 인턴님께서 답변을 주셔서 좀 더 넓은 시야로 문제 해결에 대해서 탐구하게 되었습니다! 답변 정말 감사합니다!! 의문증은 혹시나 아시면 답변 부탁드립니다!! 지금은 네이티브 쿼리로 구현했지만 추후 수정 불가한 OneToMany 로 구현 해보려고 합니다! 감사합니다 :)
inheritance startegy 선택시 고려사항
0
11
0
Entity 동등성 비교
0
14
1
패키지 구분에 대해 궁금한게 있습니다
0
20
2
스프링부트 서버 에러나요
0
22
1
코드 자료
0
34
2
조회속도 개선에서 더 개선하는 방법이 궁금합니다.
0
34
2
servlet과 container에 대한 질문입니다
0
25
1
RepositoryTest의 패키지 위치가 domain인 이유
0
31
2
도메인 모델에서 관계와 규칙을 구분하는 방법
0
41
2
16 강의 메세지 retryCount의 의도
0
25
2
JPA Repository 질문이 있습니다!
1
35
2
페이지네이션 처리를 쿼리에서 하는 방식 질문
1
37
1
UserService, CertificationService 책임 분리 기준 질문
0
28
1
spring initialiser 어떤걸 선택해야될지 모르겠어요
0
33
2
구현체가 동적으로 정해질 때, 팩토리 기법을 사용하나요?
0
52
2
Json 요청 처리
1
36
2
로그아웃-logout()-2 강에서 겟방식 로그아웃 호출 후 화면이동 질문입니다.
0
27
2
Build 관련 문제 (테스트 관련 문제)
0
43
2
job, step execution 관련 질문 드립니다.
0
35
2
실무 조언 관련 질문입니다.
0
41
1
이미지 업로드와 db 트랜잭션 묶는법
0
43
1
sdk 설정 오류
0
50
2
Orchestration SAGA 패턴 보상에 대한 질문입니다.
0
52
2
[건의][6장][작전1] deprecated 메소드
1
49
2





