QueryDSL로 프로젝션으로 Dto를 직접 받는 방법은 크게 3가지가 있다. 직접 받는다기 보다는 바로 받는다. 가 맞는 것 같다.
setter가 활용되는 bean방식
필드에 직접 주입해주는 fields 방식
생성자 방식
먼저 bean 방식부터.
그 전에, jpql방식
@Test
public void findDtoBytJPQL(){
List<MemberDto> result = em.createQuery("select new study.querydsl.dto.MemberDto(m.username, m.age)" +
" from Member m", MemberDto.class)
.getResultList();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
저렇게 패키지 명까지 써 줘야 했던게 안타까웠다.
하지만 QueryDSL에선,
@Test
public void findDtoBySetter(){
List<MemberDto> result = query.select(Projections.bean(MemberDto.class, m.username, m.age))
.from(m)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
이렇게.
Projections란 QueryDSL에서 제공하는 프로젝션에 관한 static 메소드를 가지고 있는 클래스 이다.
필드 방식과 bean 방식은 QBean이라는 것을 반환한다.
Projections.bean(클래스타입, 데이터, 데이터, ...)
클래스 타입을 넣고, 그 뒤에 데이터 타입들을 넣는데, 저 때 중요한 점이, setter명과 맞아야 한다.
즉, m.username이므로, setter이름이 setUsername 이어야 한다.
만약 setter의 이름이 다르다면,
@Test
public void findDtoBySetter(){
List<MemberDto> result = query.select(Projections.bean(MemberDto.class, m.username.as("name"), m.age))
.from(m)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
저렇게 as("별칭") 해서 name으로써 가져올 수 있다. 그러면 setName()을 호출하게 된다. (만약, setter가 없을 경우는 알아서 에러를 먹는건지 확인을 해보는 건지, 그냥 안 받은 필드가 있다면 null로 처리하고 넘어간다.
그리고, 빈 기본 생성자가 있어야 한다. setter를 활용하는 거니 빈 생성자로 만들고, setter로 넣어주는 거다.
/* select
member1.username as name,
member1.age
from
Member member1 */ select
m1_0.username,
m1_0.age
from
member m1_0
별칭 사용할 경우 저렇게 as 로 된다. sql에서는 없는 걸 보니, JPA에서만 따로 처리하는 듯 하다.
memberDto = MemberDto(username=member1, age=10)
memberDto = MemberDto(username=member2, age=20)
memberDto = MemberDto(username=member3, age=30)
memberDto = MemberDto(username=member4, age=40)
잘 들어간다.
그 다음 필드에 직접 주입 시키는 거.
@Test
public void findDtoByField(){
List<MemberDto> result = query.select(Projections.fields(MemberDto.class, m.username, m.age))
.from(m)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
Projections.fields(클래스타입, 데이터 .....)
똑같다.
동작 방식은 필드에 직접 주입시킨다고 하는데,
@ToString
public class MemberDto {
private String username;
private int age;
}
이 상태로 잘 들어간다.
분명 필드가 private인데 그런데도 주입이 된다.
이거는 라이브러리에서 뭐 따로 하는 게 있다고 ..
리플렉션을 통한 접근을 한다고 한다.
리플렉션이란 객체의 구체적인 타입을 알지 못해도 해당 객체의 멤버변수, 메서드 등에 접근할 수 있도록 해주는 자바의 기능이라고 한다.
private 이라도 접근이 가능하다는 거다. private, public 이런 거 몰라도 해당 객체의 멤버변수, 메서드 등에 접근할 수 있다.
하긴 private가 유지보수를 위해 막는거지 정말로 외부에서 접근하면 안돼! 하고 막는 것이 아니다.
근데 리플렉션 방식이라도 빈 생성자는 필요하다.
/* select
member1.username,
member1.age
from
Member member1 */ select
m1_0.username,
m1_0.age
from
member m1_0
memberDto = MemberDto(username=member1, age=10)
memberDto = MemberDto(username=member2, age=20)
memberDto = MemberDto(username=member3, age=30)
memberDto = MemberDto(username=member4, age=40)
다음 생성자 주입
@Test
public void findDtoByConstructor(){
List<MemberDto> result = query.select(Projections.constructor(MemberDto.class, m.username, m.age))
.from(m)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
이것도 사용방법은 똑같다.
Projections.constructor(MemberDto.class, 데이터 ...)
아무래도 생성자 접근 방식이다 보니 해당 생성자가 있어야 하며, 당연히 데이터의 순서도 맞아야 한다.
정말로 생성자를 찾아서, 그 생성자로 만드는 것이다.
타입, 순서가 중요하고, 이름은 꼭 중요하지는 않다.
/* select
member1.username,
member1.age
from
Member member1 */ select
m1_0.username,
m1_0.age
from
member m1_0
memberDto = MemberDto(username=member1, age=10)
memberDto = MemberDto(username=member2, age=20)
memberDto = MemberDto(username=member3, age=30)
memberDto = MemberDto(username=member4, age=40)
@Test
public void findUserDto(){
List<UserDto> result = query.select(Projections.fields(UserDto.class, m.username.as("name"), m.age))
.from(m)
.fetch();
for (UserDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
필드, bean 방식일 때 이름이 안 맞는 경우,
위에서 했다.
as 해서 별칭을 붙여서 바꿔서 가져오면 된다.
/* select
member1.username as name,
member1.age
from
Member member1 */ select
m1_0.username,
m1_0.age
from
member m1_0
memberDto = UserDto(name=member1, age=10)
memberDto = UserDto(name=member2, age=20)
memberDto = UserDto(name=member3, age=30)
memberDto = UserDto(name=member4, age=40)
이제 서브쿼리를 사용하였을 때 그걸 어떻게 Dto로 넣는지 살펴보자.
@Test
public void findUserDtoSub(){
QMember mSub = new QMember("mSub");
List<UserDto> result = query.select(
Projections.fields(
UserDto.class, m.username.as("name"),
ExpressionUtils.as(JPAExpressions.select(mSub.age.max()).from(mSub), "age")
)
)
.from(m)
.fetch();
for (UserDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
ExpressionUtils라는 것이 있다.
저것도 QueryDSL꺼다. 표현식 유틸이다. 기본적으로 Q클래스들은 대상 엔티티에 대한 표현식의 모음이라고 생각하면 된다. 즉, m.username 이런 거 다 값보다는 표현식이라고 생각하면 된다.
ExpressionUtils는 말 그대로 표현식 유틸이다. 표현식 도구이다.
as는 위에 썼던 것 처럼 별칭, 무언가의 표현식에 별칭을 붙여주는 메소드이다.
저 ExpressionUtils.as()의 return 타입은 표현식이다.
그렇게 Projection.fields, 즉 프로젝션 안에 as name과 as age 두개의 표현식이 들어간 것이다.
이런 식인 셈.
@Test
public void findUserDtoConstructor(){
List<UserDto> result = query.select(Projections.constructor(UserDto.class, m.username, m.age))
.from(m)
.fetch();
for (UserDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
마지막으로 다시 constructor 방식.
앞서 말 했듯 필드명과 맞출 필요가 없다. 그냥 타입, 순서만 잘 맞추고 해당 Dto에 그 생성자가 있어야 한다.
'스프링데이터 + JPA > QueryDSL' 카테고리의 다른 글
23. 동적쿼리. BooleanBuilder 방식 (0) | 2023.12.01 |
---|---|
22. @QueryProjection (0) | 2023.12.01 |
20. 프로젝션 (0) | 2023.11.30 |
19. 상수, 문자 더하기 (0) | 2023.11.30 |
18. CASE 문 (0) | 2023.11.30 |