해결된 질문
작성
·
1.1K
·
수정됨
1
안녕하세요 영한님의 강의를 맹신하는 대학생 개발자입니다!
다름이 아니라 플랫폼을 개발하고 있는데 연관관계가 포함된 People과 PeopleProfile 사이에서 join을 통해 값을 가져오려고 합니다.
@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;
...
}
@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<>();
...
}
@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
는 값 타입으로 구현했습니다.
@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;
}
}
@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;
}
}
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.
원문과 링크 정보는 아래와 같습니다.
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.이렇게 클래스 기반의 DTO 프로젝션을 사용할 때는 JPQL 내의 constructor expressions
로 제한되며, @Query
어노테이션을 사용하여 명시적인 쿼리를 정의할 수 있습니다.
KeepHun님께서는 쿼리 결과와 DTO 생성자 간의 타입 불일치를 해결하기 위한 위 정보를 참고하시어 문제를 해결하실 수 있으시길 바랍니다.
답변 코드 적용
위와 같이 작성한 후 실행해보았습니다.
Sql
에 해당하는 객체가 하나의 반환값이 아닌 여러 개이기 때문에 에러가 발생하였습니다.튜플로 조회
제가 알기로 객체가 여러 개인 상황에서는
Tuple
로 반환받아야 한다고 알고 있습니다. 그래서 튜플로 코드를 처리하였습니다.여전히
hashtags
는 하나의 값만 조회했습니다.최선의 해결책
따라서 저는 쿼리를 분석해 본 결과
JPA
를 사용해서 모든 객체를 조회하는 경우가 아니라면 컬렉션 테이블을 조회할 수 없다는 것을 알게 되었습니다. 따라서JPQL
이 아닌네이티브 쿼리
를 사용해 구현에 성공하였습니다. 하지만 구현은 성공하였지만 아직까지 풀리지 않은 의문증이 있습니다.컬렉션 테이블에 값은 수정 불가합니다. 그렇다면
OneToMany
로 구현하고 수정 불가하게 구현하는 것과 다른 점이 무엇인가?왜
QueryDSL
은 튜플에 리스트 객체를 못 담는가?라는 의문이 들었습니다. 일단 인프런 AI 인턴님께서 답변을 주셔서 좀 더 넓은 시야로 문제 해결에 대해서 탐구하게 되었습니다! 답변 정말 감사합니다!! 의문증은 혹시나 아시면 답변 부탁드립니다!! 지금은 네이티브 쿼리로 구현했지만 추후 수정 불가한
OneToMany
로 구현 해보려고 합니다! 감사합니다 :)