결과를 DTO로 반환할 때 다음과 같은 3가지 방법이 존재한다.
// 순수 JPA
@Test
public void findDtoByJpql() {
List<MemberDto> resultList = em.createQuery("select new study.querydsl.dto.MemberDto(m.username, m.age) from Member m", MemberDto.class)
.getResultList();
for (MemberDto memberDto : resultList) {
System.out.println("memberDto = " + memberDto);
}
}
단순히 jpql을 사용할 때 DTO 적용시 매우 지저분한 모습을 볼 수 있다. DTO의 경로를 모두 String으로 정확히 입력해야하기 때문이다.
@Test
public void findDtoBySetter() {
List<MemberDto> fetch = queryFactory
.select(Projections.bean(MemberDto.class, member.username, member.age))
.from(member)
.fetch();
for (MemberDto memberDto : fetch) {
System.out.println("memberDto = " + memberDto);
}
}
Dto를 사용하기 위해 Projections을 이용해야 한다. 프로퍼티 접근 방법을 이용하기위해 bean메서드를 활용하여 위와 같이 전달한다.
당연히 프로퍼티 이름과 조회 이름이 동일해야 정확한 Setter가 작동할 것이므로 이를 잘 매핑해주어야 한다.
이름이 다를 경우 .as()를 통해 alias처리가 필요하다. 이는 밑에서 설명한 필드 접근 방식에서도 동일하다.
@Test
public void findDtoByField() {
List<MemberDto> fetch = queryFactory
.select(Projections.fields(MemberDto.class, member.username, member.age))
.from(member)
.fetch();
for (MemberDto memberDto : fetch) {
System.out.println("memberDto = " + memberDto);
}
}
위와 달리 bean메서드 대신 fields메서드를 적용하면 끝이다
@Test
public void findDtoByConstructor() {
List<MemberDto> fetch = queryFactory
.select(Projections.constructor(MemberDto.class, member.username, member.age))
.from(member)
.fetch();
for (MemberDto memberDto : fetch) {
System.out.println("memberDto = " + memberDto);
}
}
constructor 메서드로 적용해주면 된다. 적절한 생성자가 존재해야한다.
@Data
public class MemberDto {
private String username;
private int age;
public MemberDto() {
}
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
//
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
위와 같이 DTO 클래스 생성자에 @QueryProjection를 주어 DTO를 Q객체화 해서 사용할 수 있다.
이 방식을 사용할 경우 컴파일러로 타입을 체크할 수 있어 가장 안전하다.
다만 이 방식은 DTO가 querydsl에 종속적으로 변하는 관점에서 단점이 존재한다.
querydsl을 자세히 배우기전 jpa의 동적 쿼리 문제를 해결하기 위한 가장 편리한 해결책으로서 querydsl이 가치가 있다고 설명했다.
가장 핵심적인 부분이면서 어렵지 않다. 동적 쿼리를 만들기 위해 두 가지 방법을 모두 알고있어야 한다.
@Test
public void dynamicQuery_BooleanBuilder() {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember1(usernameParam, ageParam);
assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember1(String usernameParam, Integer ageParam) {
BooleanBuilder builder = new BooleanBuilder();
if (usernameParam != null) {
builder.and(member.username.eq(usernameParam));
}
if (ageParam != null) {
builder.and(member.age.eq(ageParam));
}
List<Member> result = queryFactory
.selectFrom(member)
.where(builder)
.fetch();
return result;
}
검색 필터링 기능을 예상하여 만든 코드이다.(username, age 조건)
BooleanBuilder에 조건을 추가하여 사용할 where절에 적용하는 방식이다. 코드를 보면 builder자체에 and로 하여금 자유롭게 조건기능을 적용시킬 수 있다.
usernameParam이 null이 아닐 경우 조건을 추가해주는 것, ageParam이 null이 아닐 경우 조건을 추가해주는 것으로 이해할 수 있다. querydsl의 가장 큰 장점으로 직관적이라는 것이 이런 부분에서 크게 느껴진다. 딱히 설명이 없더라도 코드만으로 이해하기 어렵지 않다.
@Test
public void dynamicQuery_WhereParam() {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember2(usernameParam, ageParam);
assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember2(String usernameParam, Integer ageParam) {
return queryFactory
.selectFrom(member)
.where(usernameEq(usernameParam), ageEq(ageParam))
.fetch();
}
private BooleanExpression usernameEq(String usernameParam) {
if (usernameParam == null) {
return null;
}
return member.username.eq(usernameParam);
}
private BooleanExpression ageEq(Integer ageParam) {
if (ageParam == null) {
return null;
}
return member.age.eq(ageParam);
}
아마 지금 방식이 동적 쿼리를 다루기 위해 가장 효과적이고 보편적일 수 있다.
Booleanbuilder 방식과 where절에 직접 조건을 전달하는 방식을 비교해보자.
//Booleanbuilder
.where(builder)
//where절에 직접
.where(usernameEq(usernameParam), ageEq(ageParam))
Booleanbuilder 방식의 경우 where(builder)만 보았을 때 조건을 예측하기 위해 builder를 생성한 함수를 찾아가 로직을 보아야겠지만, 아래의 방식의 경우 함수명과 파라미터로 하여금 더 직관적으로 이해될 수 있도록 표현 가능하다.
또한 직접 BooleanExpression을 전달하기 위해 만든 메서드들은 재활용이 가능하여 .and .or과 같이 기존에 만든 메서드들로 추가적인 조합을 만들어낼 수 있어 재사용성이 높다.
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(28)) .execute();
크게 특별할 것은 없지만 벌크 연산은 DB에 곧바로 보내는 연산이기에 영속성 컨텍스트가 이를 따라가지 못한다. 그러므로 벌크 연산후에는 항상 em.clear()나 관련 어노테이션을 통해 영속성 컨텍스트를 초기화 해주도록 한다.
@Test
public void sqlFunction() {
List<String> fetch = queryFactory
.select(Expressions.stringTemplate("function('replace', {0}, {1}, {2})", member.username, "member", "M"))
.from(member)
.fetch();
for (String s : fetch) {
System.out.println("s = " + s);
}
}
lower와 같은 매우 보편화된 sql 함수의 경우 querydsl에서 이를 지원하여 쉽게 적용할 수 있는데 위와 같은 코드가 아래처럼 사용할 수도 있다.
@Test
public void sqlFunction2() {
List<String> fetch = queryFactory
.select(member.username)
.from(member)
.where(member.username.eq(
member.username.lower()
))
.fetch();
for (String s : fetch) {
System.out.println("s = " + s);
}
}