프로젝션과 결과 반환 - 기본
import static study.querydsl.entity.QMember.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@Test
public void simpleProjection() {
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
@Test
public void tupleProjection() {
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);
}
}
}
프로젝션 대상이 하나
- 타입을 명확하게 지정할 수 있다
프로젝션 대상이 둘 이상
튜플이나 DTO로 조회
com.querydsl.core.Tuple
은 리포지토리 계층에서만 쓰고 서비스나 컨트롤러 계층은 모르는 것이 좋다(의존성 최소화)
1. 프로젝션과 결과 반환 - DTO 조회(순수 JPA에서 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;
}
}
import static study.querydsl.entity.QMember.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@Test
public void findDtoByJPQL() {
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이름을 다 적어줘야해서 지저분하다
생성자 방식만 지원한다
2. 프로젝션과 결과 반환 - DTO 조회(Querydsl 빈 생성)(Bean population)
import static study.querydsl.entity.QMember.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@Test
public void findDtoBySetter() {
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);
}
}
@Test
public void findDtoByField() {
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);
}
}
@Test
public void findDtoByConstructor() {
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);
}
}
}
3가지 방법을 지원한다
프로퍼티 접근(Setter 접근)
필드 직접 접근
생성자 사용
3. 프로젝션과 결과 반환 - DTO 조회(프로퍼티나, 필드 접근 방근 방식에서 이름이 다를 때 해결 방안)
- UserDto
@Data
public class UserDto {
private String name;
private int age;
public UserDto() {
}
public UserDto(String name, int age) {
this.name = name;
this.age = age;
}
}
import static study.querydsl.entity.QMember.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@Test
public void findUserDtoByField() {
QMember memberSub = new QMember("memberSub");
List<UserDto> result = queryFactory
.select(
Projections.fields(
UserDto.class,
member.username.as("name"),
ExpressionUtils.as(
JPAExpressions.select(memberSub.age.max()).from(memberSub)
, "age"
)
)
)
.from(member)
.fetch();
for (UserDto userDto : result) {
System.out.println("memberDto = " + userDto);
}
}
@Test
public void findUserDtoByConstructor() {
List<UserDto> result = queryFactory
.select(
Projections.constructor(
UserDto.class,
member.username,
member.age
)
)
.from(member)
.fetch();
for (UserDto userDto : result) {
System.out.println("userDto = " + userDto);
}
}
}
컬럼명이 다른 경우 필드에 별칭을 적용할 수 있다
QType.필드.as("별칭")
ExpressionUtils.as(필드 or 서브쿼리, "별칭")
생성자 방식을 사용할 경우 DTO에 파라미터 타입이 맞는 생성자가 있어야 한다
4. 프로젝션과 결과 반환 - DTO 조회(@QueryProjection
)
@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;
}
사용 방법
사용할 생성자에
@QueryProjection
를 추가한다compileQuerydsl를 실행한다
QMemberDto 생성된다
import static study.querydsl.entity.QMember.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@Test
public void findDtoByQueryProjection() {
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
}
장점
컴파일러로 타입을 체크할 수 있으므로 가장 안전한 방법이다
생성자 사용 방법은 실행 중 생성자가 맞지 않으면 런타입 에러가 발생한다
DTO의 어떤 생성자가 있는지 미리 알 수 없다
에디터로 파라미터에 무엇이 필요한지 알 수 있다
- 생성자 사용 방법은 파라미터에 무엇을 넣어야하는지 미리 알 수 없다
단점
DTO에 QueryDSL 어노테이션을 유지해야 하고 DTO까지 Q파일을 생성해야 한다
DTO는 리포지토리, 서비스, 컨트롤러에서 많이 사용될 수 있는데 QueryDSL에 의존하게 된다
DTO의 의존성 최소와 실용성 사이에서 상황에 맞게 적합하게 선택해야 한다
동적 쿼리
1. BooleanBuilder
import static study.querydsl.entity.QMember.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@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 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();
}
}
2. Where 다중 파라미터 사용
import static study.querydsl.entity.QMember.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@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 usernameCond, Integer ageCond) {
return queryFactory
.selectFrom(member)
.where(usernameEq(usernameCond), ageEq(ageCond))
// .where(allEq(usernameCond, ageCond))
.fetch();
}
private BooleanExpression usernameEq(String usernameCond) {
return usernameCond != null ? member.username.eq(usernameCond) : null;
}
private BooleanExpression ageEq(Integer ageCond) {
return ageCond != null ? member.age.eq(ageCond) : null;
}
private BooleanExpression allEq(String usernameCond, Integer ageCond) {
return usernameEq(usernameCond).and(ageEq(ageCond));
}
}
where 조건에 null 값은 무시된다
장점
적절한 메서드명을 통해 쿼리 자체의 가독성이 높아진다
메서드를 다른 쿼리에서도 재활용 할 수 있다
메서드의 조합이 가능하다
주의사항
- null 체크를 주의해야 한다
수정, 삭제 벌크 연산
import static com.querydsl.jpa.JPAExpressions.*;
import static study.querydsl.entity.QMember.*;
import static study.querydsl.entity.QTeam.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@Test
public void bulkUpdate() {
//member1 = 10 -> 영속성 컨텍스트 member1
//member2 = 20 -> 영속성 컨텍스트 member2
//member3 = 30 -> 영속성 컨텍스트 member3
//member4 = 40 -> 영속성 컨텍스트 member3
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(28))
.execute();
//member1 = 10 -> 영속성 컨텍스트 member1 DB 비회원
//member2 = 20 -> 영속성 컨텍스트 member2 DB 비회원
//member3 = 30 -> 영속성 컨텍스트 member3
//member4 = 40 -> 영속성 컨텍스트 member3
em.flush();
em.clear();
//member1 = 10 -> 영속성 컨텍스트 비회원 DB 비회원
//member2 = 20 -> 영속성 컨텍스트 비회원 DB 비회원
//member3 = 30 -> 영속성 컨텍스트 member3
//member4 = 40 -> 영속성 컨텍스트 member3
List<Member> result = queryFactory
.selectFrom(member)
.fetch();
for (Member member : result) {
System.out.println("member = " + member);
}
}
@Test
public void bulkAdd() {
long count = queryFactory
.update(member)
.set(member.age, member.age.add(1))
// .set(member.age, member.age.multiply(2))
.execute();
}
@Test
public void bulkDelete() {
long count = queryFactory
.delete(member)
.where(member.age.gt(18))
.execute();
}
}
주의사항
- JPQL 배치와 마찬가지로, 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에 배치 쿼리를 실행하고 나면 영속성 컨텍스트를 초기화 하는 것이 안전하다
SQL function 호출하기
import static com.querydsl.jpa.JPAExpressions.*;
import static study.querydsl.entity.QMember.*;
import static study.querydsl.entity.QTeam.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@Test
public void sqlFunction() {
List<String> result = queryFactory
.select(
Expressions.stringTemplate(
"function('replace', {0}, {1}, {2})", member.username, "member", "M"
)
)
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
@Test
public void sqlFunction2() {
List<String> result = queryFactory
.select(member.username)
.from(member)
.where(member.username.eq(member.username.lower()))
// .where(member.username.eq(
// Expressions.stringTemplate("function('lower', {0})", member.username)
// )
// )
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
}
- SQL function은 JPA와 같이 Dialect에 등록된 내용만 호출할 수 있다
참고 자료