일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 프로젝트 환경설정
- 김영한
- 기본문법
- jpa 활용
- JPA 활용 2
- 스프링 데이터 JPA
- Spring Data JPA
- API 개발 고급
- 벌크 연산
- 스프링
- JPQL
- JPA
- 컬렉션 조회 최적화
- 실무활용
- 타임리프
- 임베디드 타입
- 로그인
- Bean Validation
- 트위터
- 검증 애노테이션
- JPA 활용2
- 스프링MVC
- 페이징
- 불변 객체
- 타임리프 문법
- 값 타입 컬렉션
- 스프링 mvc
- QueryDSL
- 일론머스크
- 예제 도메인 모델
- Today
- Total
RE-Heat 개발자 일지
[Querydsl] [4] 중급 문법 (상편) - 프로젝션 반환·@QueryProjection 본문
인프런 김영한 님의 강의를 듣고 작성한 글입니다.
[1] 프로젝션과 결과 반환 - 기본
■ 프로젝션이란?
엔티티 전체를 가져오는 게 아니라 조회 대상을 지정해 원하는 값만 조회하는 것을 말한다.
① 프로젝션 대상의 하나
@Test
public void simpleProjections() throws Exception {
List<String> result = queryFactory.select(member.username)
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
② 프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회
▶ 튜플 조회
@Test
public void tupleProjection() throws Exception {
List<Tuple> result = queryFactory.select(member.username, member.age)
.from(member)
.fetch();
for (Tuple tuple : result) {
String username = tuple.get(member.username);
Integer age = tuple.get(member.age);
System.out.println("username = " + username);
System.out.println("age = " + age);
}
}
- Querydsl이 값이 여러 개면 기본적으로 튜플을 사용함.
- select절 대상이 두 개이며 username과 age의 타입이 다르므로 튜플로 받음
- 튜플은 리포지토리 단계에서만 쓰고 바깥으로 내보낼 땐 DTO로 바꿔 반환하는 게 낫다. 다른 사람에게 어떤 방식을 쓰는지 알리는 건 보안상 좋지 않기 때문이다.
[2] 프로젝션과 결과 반환 - DTO 조회
MemberDto
@Data
@NoArgsConstructor
public class MemberDto {
private String username;
private int age;
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
순수 JPA에서 DTO로 반환하는 코드
@Test
public void findDtoByJPQL() throws Exception {
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에서 DTO를 조회할 땐 new 명령어를 써야 함.
- DTO package 이름을 다 적어줘야 해서 번거로움
- 생성자 방식만 지원함
■ Querydsl 빈 생성(Bean poplulation)
① 프로퍼티 접근 - Setter
@Test
public void findDtoBySetter() throws Exception {
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
- Projections.bean은 setter를 통해 데이터를 인젝션 해준다.
- 꼭 기본 생성자를 만들어야 한다. 그렇지 않으면 아래와 같은 오류 화면이 나온다.
- (기본 생성자를 만들거나 롬복의 @NoArgsConstructor를 쓰면 편하다)
- @NoArgsConstructor에 protected 달아도 오류가 뜬다.
② 필드 직접 접근
@Test
public void findDtoByField() throws Exception {
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
- Projections.fields는 getter·setter를 쓰는 대신 바로 필드로 직접 접근한다.
③ 생성자 사용
@Test
public void findDtoByConstructor() throws Exception {
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
④ 별칭이 다를 때
UserDto
@Data
@NoArgsConstructor
public class UserDto {
private String name;
private int age;
public UserDto(String name, int age) {
this.name = name;
this.age = age;
}
}
□ 필드 사용
1. 별칭이 달라 null값이 나오는 케이스
@Test
public void findUserDtoV1() throws Exception {
List<UserDto> fetch = queryFactory
.select(Projections.fields(UserDto.class,
member.username,
member.age))
.from(member)
.fetch();
}
2. 별칭을 제대로 맞추는 방법 - 해결책
@Test
public void findUserDto() throws Exception {
List<UserDto> result = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"),
))
.from(member)
.fetch();
for (UserDto userDto : result) {
System.out.println("userDto = " + userDto);
}
}
- 방법 1 : .as("name")으로 별칭을 맞춰주면 된다.
- 방법 2 : ExpressionsUtils.as(source, alias) : 필드, 서브 쿼리에 별칭 사용
- 대부분 as를 쓴다. 단, 서브 쿼리는 ExpressionUtils.as를 사용해야 한다.
3. 서브쿼리까지 포함한 방법
@Test
public void findUserDto() throws Exception {
QMember memberSub = new QMember("memberSub");
List<UserDto> result = queryFactory
.select(Projections.fields(UserDto.class,
ExpressionUtils.as(member.username, "name"),
ExpressionUtils.as(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub), "age")
))
.from(member)
.fetch();
for (UserDto userDto : result) {
System.out.println("userDto = " + userDto);
}
}
- 서브쿼리를 쓸 때 기존 alias와 다르게 쓰는 것처럼 memberSub를 따로 만들어줘야 함.
- JPAExpressiosn를 쓰고 기존처럼 만들면 된다.
- 서브쿼리 관련 구체적인 내용은 [Querydsl] [3] 기본 문법 (하편) [11] 서브쿼리 확인
□ 생성자 사용
@Test
public void findUserDtoByConstructor() throws Exception {
List<UserDto> result = queryFactory
.select(Projections.constructor(UserDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (UserDto userDto : result) {
System.out.println("memberDto = " + userDto);
}
}
- 생성자는 필드 이름이 아닌 type으로 판단하므로 이름은 상관없다.
- 세터나 필드는 이름을 매칭하는 게 중요해서 as로 별칭을 붙여야만 한다.
[3] 프로젝션과 결과 반환 - @QueryProjection
어떻게 보면 궁극의 방법이다. 그러나 dto가 Querydsl에 의존해야 하는 단점이 있다.
■ 세팅 방법
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;
}
}
- 프로젝션 결과를 반환할 DTO의 생성자에 @QueryProjection을 붙여 준 뒤 gradle → tasks → other → compileQuerydsl을 써 Q타입 DTO를 생성한다.
테스트 코드
@Test
public void findDtoByQueryProjection() throws Exception {
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
기존 생성자 방식과 @QueryProjection의 차이점
① 생성자 방식은 런타임 시점에 오류가 잡히는 반면 @QueryProjection 방식은 컴파일 시점에 오류가 뜬다.
@QueryProjection의 단점
1. Q파일을 생성해야 한다.
2. Dto는 Querydsl을 쓰는 걸 전혀 몰랐으나 @QueryProjection을 쓰면서 Querydsl에 의존하게 됐다.
- 따라서 만일 Querydsl을 쓰지 않기로 하면 오류가 뜬다.
- dto를 깔끔하게 가지고 가고 싶으면 이 방식을 쓰기 애매하다.
결론 : Querydsl을 많이 쓰면 편의상 사용하는 것으로 합의한다.
■ Distinct - 중복 제거 SQL
List<String> result = queryFactory
.select(member.username).distinct()
.from(member)
.fetch();
select 옆에 .distinct()를 붙여주면 된다.
'백엔드 > Querydsl' 카테고리의 다른 글
[Querydsl] [5] 실무 활용 - 순수 JPA와 Querydsl (0) | 2023.09.30 |
---|---|
[Querydsl] [4] 중급 문법 (하편) - 동적 쿼리·벌크 연산·SQL function (0) | 2023.09.30 |
[Querydsl] [3] 기본 문법 (하편) (0) | 2023.09.17 |
[Querydsl] [3] 기본 문법 (상편) (0) | 2023.09.17 |
[Querydsl] [2] 예제 도메인 모델 (0) | 2023.09.15 |