요즘 querydsl에서 굉장히 유용하게 사용하고 있는 기능이 있는데, 바로 transform이다. document
import static com.querydsl.core.group.GroupBy.*;
Map<Integer, List<Comment>> results = query.from(post, comment)
.where(comment.post.id.eq(post.id))
.transform(groupBy(post.id).as(list(comment)));
다음과 같이 querydsl에선 결과 값을 불러온 후 메모리에서 원하는 자료형으로 변환할 수 있는 기능을 제공한다.
함수 이름이 groupBy 라고 해서 sql의 group by와 혼동될 수 있는데, query에서 group by를 사용하지 않는다.
대신 aggregation에 필요한 데이터를 select절에 추가하는 역할과 select한 결과 값을 aggregation 하는 역할을 한다.
백문이불여일견 예시로 살펴보자
MemberEntity.java
@Entity(name = "member")
public class MemberEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Long teamId;
}
TeamEntity.java
@Entity(name = "team")
public class TeamEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String country;
}
다음과 같이 team table과 이 team에 속한 member 테이블이 있다고 하자.
그리고 team에는 country라는 속성이 있다.
그리고 나라별로 member 데이터를 모으고 싶을때,
select member.*, team.country
from member
left join member.team_id = team.id
다음과 같은 쿼리를 사용할 것이다.
그러면 이 select를 projection 혹은 tuple을 사용하여 가져와야 하는데, 일단 projection을 사용해보자
@Getter
public class MemberByCountryProjection implements Serializable {
String country;
MemberEntity member;
@QueryProjection
public MemberByCountryProjection(final MemberEntity member, final TeamEntity team) {
if (member == null || team == null) {
return;
}
this.member = member;
this.country = team.getCountry();
}
}
다음과 같이 member entity와 team entity를 가져오는 projection을 생성하고
@Override
public Collection<MemberByCountryProjection> findAllWithCountry() {
QMemberByCountryProjection selectExpression = new QMemberByCountryProjection(member, team);
return from(member)
.select(selectExpression)
.leftJoin(team)
.on(member.teamId.eq(team.id))
.fetch();
}
이 projection을 사용하여 다음과 같이 querydsl code를 짤 수 있다.
그런데 문제는
@Test
@Transactional
public void 프로젝션을_사용하여_나라와_함께_팀의_멤버들을_조회_할_수_있다() {
// given
createMember(3, createTeam("Korea"));
createMember(3, createTeam("Japan"));
// when
Map<String, List<MemberEntity>> actual = memberRepository.findAllWithCountry()
.stream()
.collect(Collectors.groupingBy(
MemberByCountryProjection::getCountry,
HashMap::new,
Collectors.mapping(MemberByCountryProjection::getMember, Collectors.toList())));
// then
assertEquals(actual.get("Korea").size(), 3);
assertEquals(actual.get("Japan").size(), 3);
}
이 결과 값을 사용하는 함수에서 Map으로 변환하는 과정을 한번 더 거쳐야 한다는 것이다.
projection도 새롭게 생성하고 결과 값을 변환하는 코드도 사용하는 함수에서 매번 작성해주어야 한다.
transform을 사용하면 좀 더 간단하게 바꿀 수 있다.
@Override
public Map<String, List<MemberEntity>> groupByCountry() {
return from(member)
.leftJoin(team)
.on(member.teamId.eq(team.id))
.transform(GroupBy.groupBy(team.country).as(list(member)));
}
바로 다음과 같이 작성하면 된다. projection을 추가적으로 생성하거나 Map으로 변환하는 코드를 작성하지 않아도 된다!
@Test
@Transactional
public void 나라별로_팀의_멤버들을_그룹핑할_수_있다() {
// given
createMember(3, createTeam("Korea"));
createMember(3, createTeam("Japan"));
// when
Map<String, List<MemberEntity>> actual = memberRepository.groupByCountry();
// then
assertEquals(actual.get("Korea").size(), 3);
assertEquals(actual.get("Japan").size(), 3);
}
모든 쿼리마다 projection을 생성하지 않아도 되고, 해당 함수를 사용하는 곳에서도 추가적인 변환을 하지 않아도 된다.
Hibernate:
select
teamentity1_.country as col_0_0_,
memberenti0_.id as col_1_0_,
memberenti0_.id as id1_0_,
memberenti0_.name as name2_0_,
memberenti0_.team_id as team_id3_0_
from
member memberenti0_
left outer join
team teamentity1_
on (
memberenti0_.team_id=teamentity1_.id
)
projection을 사용한 쿼리, transform을 생성한 쿼리 모두 같은 쿼리를 사용한다.
transform에 사용되는 필드만 select절에 추가되는 것이다.
필자의 경우는 협업을 하며 코딩을 할 때, 무작위적으로 필요없는 projection과 converter들이 남용되고 있었고, 혹은 기존에 있던 projection에 쿼리를 끼워 맞추느라 필요없는 필드도 select가 되는 문제점이 있었다.
transform을 사용하면 필요한 필드를 원하는 형태로 가져올 수 있기 때문에 이전 문제점들이 줄어든다는 것이 너무 큰 장점이었다!
Map뿐만 아니라 Collection, min, max등 다양한 함수를 제공하기 때문에 적재적소에 사용하면 굉장히 좋은 기능이다.
작성한 코드는 모두 깃허브에 저장해두었다.
'Java > JPA' 카테고리의 다른 글
[JPA] JPQL, QueryDsl, Spring Data의 exists (0) | 2022.07.31 |
---|---|
[JPA] Spring Jpa Repository의 영속성 컨텍스트 동작방식 (0) | 2022.04.17 |
[JPA] Spring Data에서 save()와 saveAll()의 성능 차이 (0) | 2022.04.04 |