프로젝션이란, SELECT 절에 조회할 대상을 지정하는 것 이다.
select문으로 검색하는 내용이 무엇이냐는 것!
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
위 코드는 프로젝션이 String을 대상으로 하나 뿐이다.
프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있다.
Member 클래스를 조회하는 것도 프로젝션이 대상이 하나라고 한다.
프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회해야한다.
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);
}
이렇게 대상이 여러개이면 Tuple 타입으로 검색하게 되고, Tuple에서 원하는값을 뽑아 써야한다.
이 튜플을 말고 DTO로 뽑아 쓸 수도 있다.
JPQL의 경우 DTO로 뽑으려면 new 연산자를 사용하여 DTO의 package이름을 다 적어줘야해서 지저분하다.
Querydsl의 DTO 반환은 3가지 방식을 지원한다.
프로퍼티 접근 - setter
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
당연히 setter가 열려있어야 한다.
필드 직접 접근
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
생성자 사용
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
파라미터에 맞는 생성자가 있어야 한다.
속성의 이름이 다를 경우
List<UserDto> fetch = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"),
ExpressionUtils.as(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub), "age")
)
).from(member)
.fetch();
ExpressionUtils.as(source,alias)
: 필드나, 서브 쿼리에 별칭 적용
username.as("memberName")
: 필드에 별칭 적용
Member의 username과 DTO의 name의 이름이 달라 as로 맞춰준 것,
앞에서 보았듯 서브쿼리에 사용되는 Qclass의 인스턴스는 내외부가 달라야 한다.
위 DTO 반환을 어노테이션으로 해결할 수도 있는데..
@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 어노테이션을 달고,
Qclass를 새로 빌드하면, 어노테이션이 달린 Qclass가 생성된다.
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
그러면 무려 위와같이 간단해진다.
이 방법은 컴파일러로 타입을 체크할 수 있으므로 가장 안전한 방법이다. 다만 DTO에 Querydsl 어노테이션을 유지해야 하는 점과 DTO까지 Q파일을 생성해야 하는 단점이 있다.
이 방법을 사용하기 위해선 Querydsl를 추후에 사용하지 않게될 가능성이 있는지.. 고민 해봐야한다고 한다.
Querydsl에 의존적이기 때문에!.
드디어, 동적 쿼리의 처리 부분을 알아보자.
동적 쿼리는 두가지 방식으로 처리할 수 있다.
BooleanBuilder
BooleanBuilder 클래스를 설정하고
들어온 파라미터들의 null 여부를 검사하여, 조건을 달아준다.
앞서 배웠듯 Querydsl은 null이 들어오면 무시하기 때문에.
JPQL과 차이가 있다.
where 다중 파라미터
where문에 각 파라미터가 null인지 체크 하는 부가 메서드를 생성한다.
나는 처음 두 방법을 비교했을 때, 첫번째 방식이 직관적이고 이해하기 쉬워서 더 좋은 방식일거라고 생각했다.
그런데, 두번째 방식이 가독성이 좋고 메서드의 재활용성이 올라간다고 한다.
기본적으로 실무에서 이런 부가 메서드들은 잘 보지 않기때문에, 코드가 깔끔한게 더 좋고, 세부 메서드가 궁금해도 타고 들어갈수 있기 때문에 확인도 좋다고 한다..
그리고 가장 최고의 장점은
private BooleanExpression allEq(String usernameCond, Integer ageCond) {
return usernameEq(usernameCond).and(ageEq(ageCond));
}
위와 같이 조건문을 묶어서 조합하여 사용가능 하다.
// 28살 미만의 데이터 이름 바꾸기
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(28))
.execute();
// 전체 나이 1살 증가
long count = queryFactory
.update(member)
.set(member.age, member.age.add(1))
.execute();
// 18살 초과 데이터 삭제
long count = queryFactory
.delete(member)
.where(member.age.gt(18))
.execute();
.execute()
를 활용한다.
JPQL 배치와 마찬가지로, 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에 배치 쿼리를 실행하고 나면 영속성 컨텍스트를 초기화 하는 것이 안전하다.