내가 공부한 것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼 겸 상세히 기록하고 얕은 부분들은 가볍게 포스팅하겠습니다.
![[JPA] QueryDSL 중급문법 - 1 [JPA] QueryDSL 중급문법 - 1](http://t1.daumcdn.net/tistory_admin/static/images/xBoxReplace_250.png)
1. 프로젝션과 결과 반환 - 기본
프로젝션이란 열 단위로 조회하는 것을 의미한다. (select는 행 단위 조회)
- 프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있다.
- 프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회할 수 있다.
1-1) 프로젝션 대상이 하나인 경우
@Test
public void simple_projection_test() {
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
for (String name : result) {
System.out.println("name = " + name);
}
}
프로젝션 대상이 하나면 명확하게 타입을 지정해줄 수 있다. List<String>
1-2) 프로젝션 대상이 둘 이상인 경우
@Test
public void tuple_projection_test() {
List<Tuple> tuples = queryFactory
.select(member.username, member.age) // 프로젝션 대상이 둘 이상
.from(member)
.fetch();
for (Tuple tuple : tuples) {
String username = tuple.get(member.username);
Integer age = tuple.get(member.age);
System.out.println("username = " + username + ", age = " + age);
}
}
Tuple은 Repository 계층 안에서 사용하는 것은 좋지만, 그 밖의 계층에서는 DTO로 변환시켜 사용하는 것 이 좋다.
Tuple을 열어보면 package가 com.querydsl.core에 있다. 즉, Tuple도 querydsl 에 종속적인 기술이다.
repository 안에서만 사용하고, 외부로 노출될때는 DTO로 변환해서 나가기를 권장한다.
하부 구현에 queryDsl을 사용한다는 것을 controller, service 계층에서 알게되면 향후 repository의 구현체를 변경할때 문제가 생긴다.
2. 프로젝션과 결과 반환 - DTO 조회
이번 시간에는 Member의 모든 필드값이 아닌, name 과 age 두건만 조회해고 싶은 상황이다.
사용할 MemberDto는 다음과 같다.
@Data
@NoArgsConstructor
public class MemberDto {
private String username;
private int age;
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
2-1) JPQL로 DTO 조회
@Test
public void findByDtoJPQL() {
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);
}
}
실행 결과는 다음과 같다.
![[JPA] QueryDSL 중급문법 - 1 -
2. 프로젝션과 결과 반환 - DTO 조회
- undefined - 2-1) JPQL로 DTO 조회 [JPA] QueryDSL 중급문법 - 1 -
2. 프로젝션과 결과 반환 - DTO 조회
- undefined - 2-1) JPQL로 DTO 조회](https://blog.kakaocdn.net/dn/6h0lJ/btrB59gkPf9/obZmCP6rv55ODZhaqFOo3K/img.png)
실행되는 쿼리는 다음과 같다.
select
member0_.username as col_0_0_,
member0_.age as col_1_0_
from
member member0_
하지만 이 방식은 사용하기 너무 번잡하다. 일단 페키지 명을 전부 다 적어줘야하는 점부터가 고난길이다...
new 명령어도 사용해야하고, 생성자 방식만 지원한다.
2-2) QueryDSL 에서의 DTO 반환하기
QueryDsl 에서는 총 3가지 방식의 DTO반환 방식을 지원한다.
- 프로퍼티 접근
- 필드 직접 접근
- 생성자 사용
▶ 프로퍼티 접근 - setter
@Test
public void findByDtoSetter() {
List<MemberDto> memberDtoList = queryFactory
.select(Projections.bean(MemberDto.class, member.username, member.age))
.from(member)
.fetch();
for (MemberDto memberDto : memberDtoList) {
System.out.println("memberDto = " + memberDto);
}
}
이 방식은 setter가 DTO 상에 있어야 사용 가능한 방식이다.
롬복을 통해 @Setter를 지정해 주거나, @Data 를 사용해야 한다.
▶ 필드 직접 접근
필드 방식은 getter, setter가 DTO에 없어도 적용할 수 있는 방식이다.
필드가 private 여도 알아서 다 처리된다.
@Test
public void findByDtoField() {
List<MemberDto> memberDtoList = queryFactory
.select(Projections.fields(MemberDto.class, member.username, member.age))
.from(member)
.fetch();
for (MemberDto memberDto : memberDtoList) {
System.out.println("memberDto = " + memberDto);
}
}
Projections.fields(주입대상class, 필드1, 필드2, ...) 처럼 사용된다.
▶ 프로퍼티, 필드 직접 접근 - 별칭이 다른 경우
다음과 같이 UsetDto를 따로 만들어 사용해보자.
@Data
@NoArgsConstructor
public class UserDto {
private String name;
private int age;
public UserDto(String name, int age) {
this.name = name;
this.age = age;
}
}
따라서 실행해볼 test 코드는 다음과 같다. 반환 타입이 UserDto로 바뀌게 되었다.
@Test
public void findByUserDtoField() {
List<UserDto> memberDtoList = queryFactory
.select(Projections.fields(UserDto.class, member.username, member.age))
.from(member)
.fetch();
for (UserDto userDto : memberDtoList) {
System.out.println("userDto = " + userDto);
}
}
실행시 정상적으로 실행된다. 다만 username에 대한 데이터를 가져오지 못하게 된다.
우리가 fields()에 인자로 넘기는 이름은 member.username인데, DTO 필드 이름은 name이다.
![[JPA] QueryDSL 중급문법 - 1 -
2. 프로젝션과 결과 반환 - DTO 조회
- undefined - 2-2) QueryDSL 에서의 DTO 반환하기 [JPA] QueryDSL 중급문법 - 1 -
2. 프로젝션과 결과 반환 - DTO 조회
- undefined - 2-2) QueryDSL 에서의 DTO 반환하기](https://blog.kakaocdn.net/dn/cNvA3V/btrB5JPIlZS/gqzidyIrfRmPLJbDtetIa1/img.png)
이럴때는 member.username.as("name") 으로 사용하자.
.select(Projections.fields(UserDto .class, member.username.as("name"), member.age))
이렇게 별칭을 맞추어 주면 정상 작동한다. 이는 필드에 별칭을 주는 방식이다.
서브 쿼리에 별칭을 적용하는 방법을 알아보자.
@Test
public void findByUserDtoField() {
QMember memberSub = new QMember("memberSub");
List<UserDto> memberDtoList = queryFactory
.select(Projections.fields
(
UserDto.class,
member.username.as("name"),
ExpressionUtils.as(JPAExpressions.select(memberSub.age.max()).from(memberSub), "age")
)
)
.from(member)
.fetch();
for (UserDto userDto : memberDtoList) {
System.out.println("userDto = " + userDto);
}
}
형식이 fields(UserDto.class, 이름관련, 나이관련) 와 같은 형식이다.
여기서 "이름관련" 부분은 'name'이라는 별칭을 지정해 주었다.
"나이관련" 부분에는 JPAExpression을 통해 subQuery를 지정해 주었는데, subQuery에는 별칭이 없다.
따라서 ExpressionUtils.as()로 한번 wrapping하여 "age" 라는 별칭을 추가해준 것 이다.
"이름관련" 부분에도 as("name")이 아니라, 다음과 같이 사용해도 된다.
ExpressionUtils.as(member.username, "name")
▶ 생성자 사용
@Test
public void findByDtoConstructor() {
List<MemberDto> memberDtoList = queryFactory
.select(Projections.constructor(MemberDto.class, member.username, member.age))
.from(member)
.fetch();
for (MemberDto memberDto : memberDtoList) {
System.out.println("memberDto = " + memberDto);
}
}
생성자 같은 경우 이름이 아니라 type을 보고 만들어지기 때문에 직전의 field 방식의 문제가 생기지 않는다.
3. 프로젝션과 결과 반환 - @QueryProjection
이번 방법은 궁극의 방법이자, 단점도 많은 방식이다.
MemberDto의 생성자에 바로 @QueryProjection 을 추가해주자.
@Data
@NoArgsConstructor
public class MemberDto {
private String username;
private int age;
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
이후 gradle에 가서 complieQuerydsl 을 실행해준다.
![[JPA] QueryDSL 중급문법 - 1 -
3. 프로젝션과 결과 반환 - @QueryProjection
[JPA] QueryDSL 중급문법 - 1 -
3. 프로젝션과 결과 반환 - @QueryProjection](https://blog.kakaocdn.net/dn/bTGeTe/btrB6u5Coh0/SMk2YK4H136iWCH2DEFoW1/img.png)
위와같이 QMemberDto가 생성된것을 확인할 수 있다.
이후 그냥 생성자를 통해 객체를 생성하듯이 넘겨주면 된다.
@Test
public void findDtoByQueryProjection() {
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
그럼 위 방식의 생성자를 사용하는 방식과, 이전시간에 배웠던 Projections.constructor는 뭐가 다를까?
Projections.constructor 방식은 다음과 같이 생성자에 추가적으로 값을 더 넘기게 되었을때 (id를 추가로 전달)
List<MemberDto> memberDtoList = queryFactory
.select(Projections.constructor(MemberDto.class, member.id, member.username, member.age))
.from(member)
.fetch();
컴파일 오류가 아니라, 실행이 된 후 runtime 오류가 발생한다.
유저가 실제로 해당 기능을 사용하는 순간에 오류가 발생하는 것 이다.
똑같은 문제를 @QueryProjection 방식에서는 컴파일 오류로 알려준다.
![[JPA] QueryDSL 중급문법 - 1 -
3. 프로젝션과 결과 반환 - @QueryProjection
[JPA] QueryDSL 중급문법 - 1 -
3. 프로젝션과 결과 반환 - @QueryProjection](https://blog.kakaocdn.net/dn/NqGCx/btrB7OoKJ1V/21eBUA1oWSbpbfObFhOKZ1/img.png)
하지만 이 방식에도 단점은 존제한다.
- Q 클래스 파일을 만들어야 한다는 점
- 아키텍처 문제(의존관계 문제), DTO 가 QueryDSL에 대한 의존성이 생겨버린다.
실용적인 관점에서 이정도는 사용해도 된다 생각되면 사용해도 된다.
'BackEnd > JPA' 카테고리의 다른 글
[JPA] 실무 활용 - 순수 JPA와 Querydsl (0) | 2022.05.15 |
---|---|
[JPA] QueryDSL 중급문법 - 2 (0) | 2022.05.14 |
[JPA] QueryDSL 기본문법 - 4 (0) | 2022.05.14 |
[JPA] QueryDSL 기본문법 - 3 (0) | 2022.05.14 |
[JPA] QueryDSL 기본문법 - 2 (0) | 2022.05.13 |
댓글