프로젝션?
- 데이터베이스에서 프로젝션은 조건에 맞는 릴레이션의 속성을 추출한다는 의미이다
- 따라서 우리는 원하는 속성을 추출하여 얻을 수 있다
- 만약에 프로젝션 대상이 한개라면 타입을 "명확"하게 정의할 수 있다
- 하지만 둘 이상이라면 "튜플"이나 "DTO"를 사용해야한다
순수 JPA에서의 DTO 조회
List<MemberDto> result = em.createQuery("select new study.querydsl.dto.MemberDto(m.username, m.age) " +
"from Member m", MemberDto.class)
.getResultList();
- 순수 JPA에서 DTO를 조회하기 위해서는 new 명령어를 이용하여 패키지 구조를 다 적어줘야함
- 또한, 생성자 방식만 지원함
위와 같은 이유로(내 생각엔 패키지 명 적는게 제일 큼) 순수 JPA에서의 DTO 조회는
코드를 지저분할 뿐만 아니라 귀찮다고 생각한다
이에 querydsl은 이러한 단점을 보완하기 위해 DTO를 반환할 때 3가지 방법을 지원한다
Querydsl에서의 DTO 조회
- querydsl 빈 생성(Bean population)이라 함
- 프로퍼티 접근, 필드 접근, 생성자 사용, 총 3가지 방법을 지원한다
MemberDto.class
@Data
@NoArgsConstructor
public class MemberDto {
private String username;
private int age;
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
프로퍼티 접근
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
필드 접근
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
만약 엔티티의 필드명과 변환할 DTO의 필드명이 다르면 어떡할까?
UserDto.class
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
private String name;
private int age;
}
@Test
@DisplayName("엔티티의 필드명과 변환할 DTO 필드명이 다를 경우 인식하지 못함")
void findUserDto() throws Exception {
List<UserDto> result = queryFactory
.select(Projections.fields(UserDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (UserDto userDto : result) {
System.out.println("userDto = " + userDto);
}
}
Member에서 name을 나타내는 필드명은 username이다
이처럼 UserDto의 필드명은 name, Member의 필드명이 username일 때, DTO를 조회하게 되면
name 필드가 제대로 인식되지 못하는 것을 알 수 있다
@Test
void findUserDto() throws Exception {
QMember memberSub = new QMember("memberSub");
List<UserDto> result = queryFactory
.select(Projections.fields(UserDto.class,
//member.username.as("name"),
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);
}
}
"as" 메서드를 사용해서 필드에 "별칭"을 적용할 수 있다
ExpressionUtils는 무엇인가??
as를 사용하면 필드에 별칭을 적용할 수 있지만
ExpressionUtils.as를 사용하면 "필드"나, "서브 쿼리"에 별칭을 적용할 수 있다
위 코드는 age의 max 값을 UserDto의 age에 적용하고 있다
생성자 사용
@Test
@DisplayName("querydsl DTO 반환 by 생성자")
void findDtoByConstructor() throws Exception {
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
List<UserDto> result2 = queryFactory
.select(Projections.constructor(UserDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
for (UserDto userDto : result2) {
System.out.println("userDto = " + userDto);
}
}
프로퍼티나 필드에 접근할 때와 다르게 생성자 사용 방법에서는 엔티티의 필드명과 변환할 DTO 필드명이 달라도 상관없다
어떻게 보면 생성자에 접근하기 때문에 달라도 상관없는 것이 당연하다 생각한다
@QueryProjection
위에서 생정자 방법을 사용하기 위해서는 select에서 Projections.constructor를 이용했다
하지만 DTO 클래스에서 생성자에 @QueryProjection 을 사용하면 간편하게 DTO를 반환할 수 있다
MemberDto.class
@Data
@NoArgsConstructor
public class MemberDto {
private String username;
private int age;
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
- @QueryProjection을 사용
- gradle -> other -> compileQuerydsl ( ./gradlew compileQuerydsl)
- QMemberDto가 생성됐는지 확인
@Test
@DisplayName("@QueryProjection 사용")
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은 컴파일 타임 때 타입을 체크할 수 있으므로 가장 안전하다
- 그러나 DTO에 @QueryProjection을 사용하면서 querydsl에 의존하게 된다
- 또한, Q 클래스를 생성해야하므로 단점이 있다
동적 쿼리
동적 쿼리를 해결하는 방식
- BooleanBuilder
- Where 다중 파라미터
BooleanBuilder
@Test
@DisplayName("BooleanBuilder 사용")
void booleanBuilder() throws Exception {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember1(usernameParam, ageParam);
assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember1(String usernameCond, Integer ageCond) {
BooleanBuilder builder = new BooleanBuilder();
if (usernameCond != null) {
builder.and(member.username.eq(usernameCond));
}
if (ageCond != null) {
builder.and(member.age.eq(ageCond));
}
return queryFactory.selectFrom(member)
.where(builder)
.fetch();
}
searchMember1 파라미터 null일 경우
List<Member> result = searchMember1(null, ageParam);
usrename 파라미터가 null이 된다면 어떤 쿼리가 나갈지 확인해보자
그러면 age 조건만 where 절에 들어간 것을 알 수 있다
where 다중 파라미터 사용
@Test
@DisplayName("where 다중 파라미터 사용")
void whereParam() throws Exception {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember2(usernameParam, ageParam);
assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember2(String usernameCond, Integer ageCond) {
return queryFactory
.selectFrom(member)
.where(usernameEq(usernameCond), ageEq(ageCond)) //null이 나오면 해당조건을 무시
.fetch();
}
private BooleanExpression usernameEq(String usernameCond) {
if (usernameCond == null) {
return null;
}
return member.username.eq(usernameCond);
}
private BooleanExpression ageEq(Integer ageCond) {
return ageCond != null ? member.age.eq(ageCond) : null;
}
- where 조건에 null 값은 무시된다
- 메서드를 다른 쿼리에서도 재활용 할 수 있다
serachMember2의 메서드에서 username이 null일 경우 age 조건만 나가는 것을 알 수 있다
@Test
void whereParam() throws Exception {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember2(usernameParam, ageParam);
assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember2(String usernameCond, Integer ageCond) {
return queryFactory
.selectFrom(member)
.where(allEq(usernameCond, ageCond))
.fetch();
}
private Predicate allEq(String usernameCond, Integer ageCond) {
return usernameEq(usernameCond).and(ageEq(ageCond));
}
- allEq와 같이 조합할 수 있다
'Spring 강의 > QueryDSL' 카테고리의 다른 글
순수 JPA Repo + Querydsl, 동적 쿼리 (0) | 2022.05.14 |
---|---|
수정, 삭제 벌크 연산 (0) | 2022.05.13 |
QueryDsl 조인 ~ (0) | 2022.05.02 |
JPQL & Querydsl , Querydsl 검색 조건, 결과(fetch), 정렬, 페이징, 집합 (0) | 2022.04.29 |