프로젝션과 결과 반환 - 기본
프로젝션은 select 절의 대상을 지정하는 것이다.
1. 프로젝션 대산이 하나일 때
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* 프로젝션이 하나일 때
*/
@Test
void simpleProjection() {
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
}
/* select
member1.username
from
Member member1 */ select
member0_.username as col_0_0_
from
member member0_
s = member1
s = member2
s = member3
s = member4
프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있다.
2. 프로젝션 대상이 둘 이상일 때
프로젝션 대상이 둘 이상일 때 튜플(Tuple)로 조회한다.
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* 프로젝션이 두 개 이상일 때
*/
@Test
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);
}
}
}
/* select
member1.username,
member1.age
from
Member member1 */ select
member0_.username as col_0_0_,
member0_.age as col_1_0_
from
member member0_
username = member1
age = 10
username = member2
age = 20
username = member3
age = 30
username = member4
age = 40
Tuple은 Querydsl의 라이브러리에 속한다. 튜플을 리포지토리 계층 안에서 사용하는 것은 괜찮은데 서비스나 컨트롤 계층에서는 사용하지 않을 것을 권장한다. 비즈니스 로직에서는 리포지토리 라이브러리를 알면 좋지 않다. 리포지토리의 하위 기술 스택을 변경할 시 비즈니스 로직도 같이 수정하는 것은 나쁜 설계다. 컨트롤러나 서비스 계층으로 전달할 때는 DTO로 변환해서 던질 것
프로젝션과 결과 반환 - DTO
먼저 DTO 객체를 정의해보자.
📌 MemberDto
package study.querydsl.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class MemberDto {
private String username;
private int age;
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
1. 순수 JPA에서 DTO 조회
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* 순수 JPA에서 DTO 조회
* 무조건 생성자 주입만 가능
*/
@Test
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);
}
}
}
/* select
new study.querydsl.dto.MemberDto(m.username,
m.age)
from
Member m */ select
member0_.username as col_0_0_,
member0_.age as col_1_0_
from
member member0_
memberDto = MemberDto(username=member1, age=10)
memberDto = MemberDto(username=member2, age=20)
memberDto = MemberDto(username=member3, age=30)
memberDto = MemberDto(username=member4, age=40)
순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용해야 한다.
그리고 DTO의 패키지명을 다 적어줘야해서 지저분하다.
참고로 생성자 주입 방식만 지원한다.
2. QueryDsl 빈 생성 (Bean population)
결과를 DTO로 반환할 때 사용한다. 총 3가지 방법을 지원한다.
- 프로퍼티 접근법
- 필드 직접 접근
- 생성자 주입
🔍 프로퍼티 접근법 - Setter
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* 프로퍼티 접근법
* setter 필요
* 기본 생성자 필요
*/
@Test
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);
}
}
}
/* select
member1.username,
member1.age
from
Member member1 */ select
member0_.username as col_0_0_,
member0_.age as col_1_0_
from
member member0_
memberDto = MemberDto(username=member1, age=10)
memberDto = MemberDto(username=member2, age=20)
memberDto = MemberDto(username=member3, age=30)
memberDto = MemberDto(username=member4, age=40)
기본 생성자 필수로 필요하다. 기본 생성자로 인스턴스 생성 후, setter로 값 주입하기 때문이다.
Projections.bean() 메서드를 이용한다. 첫 번째 인자로는 DTO 타입, 나머지 인자에서는 DTO의 setter로 주입해줄 값들을 전달하면 된다.
DTO의 필드 명과 Member의 필드 명이 일치해야 값을 넣을 수 있다. (ex. username, age)
🔍 필드 직접 접근법
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
* 필드 접근법
* getter / setter 필요 없다.
* 기본 생성자 필수
*/
@Test
void findByField() {
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);
}
}
}
/* select
member1.username,
member1.age
from
Member member1 */ select
member0_.username as col_0_0_,
member0_.age as col_1_0_
from
member member0_
memberDto = MemberDto(username=member1, age=10)
memberDto = MemberDto(username=member2, age=20)
memberDto = MemberDto(username=member3, age=30)
memberDto = MemberDto(username=member4, age=40)
Projections.fields() 메서드를 사용한다. 첫 번째 인자로는 DTO 타입, 나머지 인자에서는 DTO의 필드 값에 주입해줄 값들을 전달하면 된다.
자바 리플랙션 기술 등을 사용하여 DTO 필드에 직접 접근하여 값을 채워준다.
기본 생성자가 필수로 있어야 한다.
DTO의 필드 명과 Member의 필드 명이 일치해야 값을 넣을 수 있다. (ex. username, age)
🔍 생성자 주입
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* 생성자 주입 방식
* 필드 값 주입 순서 중요
*/
@Test
void findByConstructor() {
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);
}
}
}
/* select
member1.username,
member1.age
from
Member member1 */ select
member0_.username as col_0_0_,
member0_.age as col_1_0_
from
member member0_
memberDto = MemberDto(username=member1, age=10)
memberDto = MemberDto(username=member2, age=20)
memberDto = MemberDto(username=member3, age=30)
memberDto = MemberDto(username=member4, age=40)
Projections.constructor() 메서드를 사용한다. 첫 번째 인자로는 DTO 타입, 나머지 인자에서는 생성자에 주입해줄 값들을 전달하면 된다. 생성자에게 인자를 전달해줘야하기 때문에 생성자 인자와 순서가 같아야한다.
기본 생성자 이외에 필드 값을 주입시켜주기 위한 생성자가 필요하다.
DTO의 필드 명과 Member의 필드 명이 일치하지 않아도 타입만 일치한다면 값을 넣을 수 있다.
생성자 방식을 제외한 프로퍼티 접근 방법과 필드 직접 주입 방법을 사용하려면 DTO의 필드명과 DTO 필드 값으로 주입해줄 Q클래스의 필드명이 같아야 한다. 같지 않을 경우 별칭을 DTO의 필드명으로 지정해야 한다.
먼저 QMember 클래스의 필드명이 서로 다른 DTO를 정의해보자.
📌 UserDto
package study.querydsl.dto;
@Data
@NoArgsConstructor
public class UserDto {
private String name;
private int age;
public UserDto(String name, int age) {
this.name = name;
this.age = age;
}
}
QMember 클래스의 필드 명에서는 username이지만 UserDto 클래스의 필드 명은 name으로 서로 다르게 정의한다.
🔍 DTO 필드명과 Q클래스 필드 명이 서로 다를 때 - 별칭 사용 X
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* 필드 명이 서로 같지 않으면 매칭이 안된다.
*/
@Test
void findUserDtoNoAlias() {
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);
}
}
}
/* select
member1.username,
member1.age
from
Member member1 */ select
member0_.username as col_0_0_,
member0_.age as col_1_0_
from
member member0_
userDto = UserDto(name=null, age=10)
userDto = UserDto(name=null, age=20)
userDto = UserDto(name=null, age=30)
userDto = UserDto(name=null, age=40)
QMember 필드명과 UserDto의 필드명이 서로 다르기 때문에 UserDto의 name필드에 값이 주입되지 않아서 null 값인 것을 알 수 있다.
하지만 별칭을 사용하여 매칭해줄 수 있다.
🔍 DTO 필드명과 Q클래스 필드 명이 서로 다를 때 - 별칭 사용 O
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* .as() 메서드를 통해 인자로 별칭을 전달하여 별칭으로 매칭
*/
@Test
void findUserDtoByAlias() {
List<UserDto> result = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"),
member.age))
.from(member)
.fetch();
for (UserDto userDto : result) {
System.out.println("userDto = " + userDto);
}
}
}
/* select
member1.username as name,
member1.age
from
Member member1 */ select
member0_.username as col_0_0_,
member0_.age as col_1_0_
from
member member0_
userDto = UserDto(name=member1, age=10)
userDto = UserDto(name=member2, age=20)
userDto = UserDto(name=member3, age=30)
userDto = UserDto(name=member4, age=40)
.as() 메서드를 통해 별칭을 파라미터로 전달하여 해당 별칭으로 DTO 클래스의 필드명과 매칭한다.
member.username.as("name")과 ExpressionUtils.as(member,username, "name")은 서로 같다. 하지만 member.username.as("name")이 더 간결하다.
ExpressionUtils.as() 메서드에서 첫 번째 인자로는 필드값, 두번째 인자로는 지정할 별칭을 전달하면 된다. 이 메서드는 필드나 서브 쿼리에 별칭을 적용할 때 사용한다.
🔍 생성자 주입은 별칭이 필요 없다.
생성자 주입은 기본 생성자 이외에 필드 값을 인자로 받는 생성자의 파라미터 순서로 값을 받기에 Projections.constructor() 메서드에서 값을 전달할 인자의 순서가 중요하다.
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* 생성자 주입 방식은 필드 명이 아닌 타입으로 주입한다.
* 그래서 서로 필드명이 일치하지 않아도 된다.
*/
@Test
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);
}
}
}
/* select
member1.username,
member1.age
from
Member member1 */ select
member0_.username as col_0_0_,
member0_.age as col_1_0_
from
member member0_
userDto = UserDto(name=member1, age=10)
userDto = UserDto(name=member2, age=20)
userDto = UserDto(name=member3, age=30)
userDto = UserDto(name=member4, age=40)
생성자 주입 방식에서는 별칭을 사용하지 않아도 UserDto 필드값이 제대로 들어간 것을 확인할 수 있다.
🔍 서브쿼리 사용시 별칭으로 매칭
서브 쿼리를 사용할 경우 마찬가지로 별칭으로 매칭해야한다.
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* 서브 쿼리 사용시 별칭으로 매칭해야한다.
*/
@Test
void findUserDtoUseSubQueryByAlias() {
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("userDto = " + userDto);
}
}
}
/* select
member1.username as name,
(select
max(memberSub.age)
from
Member memberSub) as age
from
Member member1 */ select
member0_.username as col_0_0_,
(select
max(member1_.age)
from
member member1_) as col_1_0_
from
member member0_
userDto = UserDto(name=member1, age=40)
userDto = UserDto(name=member2, age=40)
userDto = UserDto(name=member3, age=40)
userDto = UserDto(name=member4, age=40)
서브쿼리 같은 경우 ExpressionUtils.as() 메서드를 통해 별칭을 지정하여 매칭해야한다.
프로젝션 결과 반환 - @QueryProjection
프로젝션 결과를 반환 방법 중에서 가장 실용적인 방법이다.
필드값을 인자로 받는 DTO 생성자에 @QueryProjection 애노테이션을 붙여주면 된다.
📌 MemberDto
package study.querydsl.dto;
import com.querydsl.core.annotations.QueryProjection;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@Data
@NoArgsConstructor
@Slf4j
public class MemberDto {
private String username;
private int age;
@QueryProjection
public MemberDto(String username, int age) {
log.info("MemberDto의 생성자가 호출되었습니다.!!");
this.username = username;
this.age = age;
}
}
@QueryProjection 애노테이션이 생성자에 적용되면 해당 클래스의 Q타입을 생성한다.
해당 Q타입의 인스턴스를 생성할 때 @QueryProjection 애노테이션이 적용된 생성자가 호출되어 생성된다.
Q타입 객체가 생성될 때 진짜로 @QueryProjection애노테이션이 적용된 생성자가 호출되는지 알아보기 위해 log.info()를 생성자 로직에 추가했다.
그리고나서 Tasks → other → compileQuerydsl 을 실행하면 @QueryPorjection 애노테이션이 적용되어 있는 MemberDto가 Q타입으로 생성된다.
QMemberDto가 생성된 것을 알 수 있다.
이제 어떻게 적용할 수 있는지 코드로 알아보자.
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* @QueryProjection 애노테이션을 통해 Q클래스 DTO의 생성자 주입을 이용한 방법
*/
@Test
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);
}
}
}
/* select
member1.username,
member1.age
from
Member member1 */ select
member0_.username as col_0_0_,
member0_.age as col_1_0_
from
member member0_
MemberDto의 생성자가 호출되었습니다.!!
MemberDto의 생성자가 호출되었습니다.!!
MemberDto의 생성자가 호출되었습니다.!!
MemberDto의 생성자가 호출되었습니다.!!
memberDto = MemberDto(username=member1, age=10)
memberDto = MemberDto(username=member2, age=20)
memberDto = MemberDto(username=member3, age=30)
memberDto = MemberDto(username=member4, age=40)
출력 결과를 보면 @QueryProjection 애노테이션이 적용된 생성자 로직안에 있는 로그가 출력됐다. 그러므로 Q타입 인스턴스가 생성될 때 @QueryProjection 애노테이션이 적용된 생성자가 호출된 것을 알 수 있다.
이 방법은 컴파일러로 타입을 체크할 수 있으므로 가장 안전한 방법이다. 다른 프로젝션 반환 방법들은 컴파일 에러를 발생시키지 않아서 애플리케이션 로딩 시점에 에러가 발생한다.
위 코드는 @QueryProjection 애노테이션을 사용하지 않고 그냥 생성자 주입을 통해 프로젝션을 반환한 코드다. MemberDto생성자에 id값을 받는 생성자 로직이 없으므로 위 코드를 실행시키면 에러가 발생한다.
이제 @QueryProjection 애노테이션을 적용하여 프로잭션을 반환한 코드를 보자.
위 코드를 보면 MemberDto의 생성자에 id값을 받는 생성자 로직이 없으므로 컴파일 에러를 발생시킨다. 가장 큰 장점이다.
@QueryProjection 애노테이션을 통해 프로젝션을 반환하는 방법이 제일 실용적이다. 하지만 이 방법에도 단점이 존재한다.
첫 째, 먼저 DTO 객체도 Q타입을 생성해야한다는 것이다.
둘 째, @QueryProjection 애노테이션이 Querydsl의 기술 스택이므로, DTO가 QueryDsl을 의존하게 된다. 일반적으로 DTO는 애플리케이션의 서비스 계층, 컨트롤러 계층에서도 쓰이고 심지어 API의 반환값으로도 쓰인다. 이렇듯, DTO는 여러 레이어에 결쳐서 사용된다. 즉, 애플리케이션의 QueryDsl 의존성이 높다는 의미다. 이것은 설계에 있어서 큰 단점이 될 수도 있다. 만약에 QueryDsl을 사용하지 않고 다른 기술 스택을 사용할 경우 변경의 범위가 너무 커진다. 하지만 앞으로 QueryDsl을 그대로 사용할 것을 명확히 정해졌다면 DTO가 QueryDsl에 의존해도 상관없다.
🧷 참고. distinct() 사용법 - 중복 제거
@Test
void name() {
List<String> result = queryFactory
.select(member.username).distinct()
.from(member)
.fetch();
}
select절 이후에 .distinct() 메서드를 통해 중복을 제거할 수 있다.
동적쿼리 - BooleanBuilder 사용
동적 쿼리를 해결하는데 두 가지 방법이 있다.
- BooleanBuilder
- Where 다중 파라미터 사용
BooleanBuilder는 쿼리의 조건 설정인 where절 뒤의 조건을 생성해주는 것이다.
null 인지 아닌지 판별하여 builder를 조립해서 끼워 넣는 방식으로 작업을 한다.
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* BooleanBuilder
*/
@Test
void dynamicQuery_BooleanBuilder() {
String usernameParam = "member1";
Integer ageParam = null;
List<Member> result = searchMember1(usernameParam, ageParam);
assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember1(String usernameParam, Integer ageParam) {
// 파라미터를 전달하여 초기값 설정 가능. 만약에 초기 값 설정시 null 값이 오면 안된다는 방어코드를 작성해야 한다.
BooleanBuilder builder = new BooleanBuilder();
if(usernameParam != null) { // null 값 방어 코드
builder.and(member.username.eq(usernameParam));
}
if (ageParam!=null) {
builder.and(member.age.eq(ageParam));
}
return queryFactory
.selectFrom(member)
.where(builder)
.fetch();
}
}
/* select
member1
from
Member member1
where
member1.username = ?1
and member1.age = ?2 */ select
member0_.member_id as member_i1_1_,
member0_.age as age2_1_,
member0_.team_id as team_id4_1_,
member0_.username as username3_1_
from
member member0_
where
member0_.username=?
and member0_.age=?
where절에 파라미터 바인딩이 2개가 된 것을 볼 수 있다.
동적 쿼리 - Where 다중 파라미터 사용
실무에서 자주 쓴다.
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* 동적 쿼리 처리
* where 다중 파라미터 사용
*/
@Test
void dynamicQuery_WhereParam() {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember2(usernameParam, ageParam);
}
private List<Member> searchMember2(String usernameParam, Integer ageParam) {
return queryFactory
.selectFrom(member)
.where(usernameEq(usernameParam), ageEq(ageParam)) // where절에 null값이 오면 null값을 무시한다.
.fetch();
}
private BooleanExpression usernameEq(String usernameParam) {
return usernameParam != null ? member.username.eq(usernameParam) : null;
}
private BooleanExpression ageEq(Integer ageParam) {
return ageParam != null ? member.age.eq(ageParam) : null;
}
}
/* select
member1
from
Member member1
where
member1.username = ?1
and member1.age = ?2 */ select
member0_.member_id as member_i1_1_,
member0_.age as age2_1_,
member0_.team_id as team_id4_1_,
member0_.username as username3_1_
from
member member0_
where
member0_.username=?
and member0_.age=?
where절 조건에 null 값은 무시된다.
메서드를 다른 쿼리에서도 재활용할 수 있다.
쿼리 자체의 가독성이 높아진다.
여러 조건을 합쳐서 하나의 메서드로 만든 후 where절에 파라미터로 전달해도 된다. 코드로 알아보자.
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* 동적 쿼리 처리
* where 다중 파라미터 사용
* 여러 조건을 합쳐서 하나의 메서드로 만들었다.
*/
@Test
void dynamicQuery_WhereComposeParam() {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember3(usernameParam, ageParam);
assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember3(String usernameParam, Integer ageParam) {
return queryFactory
.selectFrom(member)
.where(allEq(usernameParam, ageParam))
.fetch();
}
private BooleanExpression usernameEq(String usernameParam) {
return usernameParam != null ? member.username.eq(usernameParam) : null;
}
private BooleanExpression ageEq(Integer ageParam) {
return ageParam != null ? member.age.eq(ageParam) : null;
}
/**
* 여러 조건을 합칠 수 있다.
* 재사용 가능
* null 체크는 주의해서 처리해야한다.
*/
private BooleanExpression allEq(String usernameParam, Integer ageParam) {
return usernameEq(usernameParam).and(ageEq(ageParam));
}
}
/* select
member1
from
Member member1
where
member1.username = ?1
and member1.age = ?2 */ select
member0_.member_id as member_i1_1_,
member0_.age as age2_1_,
member0_.team_id as team_id4_1_,
member0_.username as username3_1_
from
member member0_
where
member0_.username=?
and member0_.age=?
ageEq() 메서드와 usernameEq() 메서드를 하나의 메서드로 합쳐서 allEq() 메서드를 새롭게 정의했다.
allEq() 메서드를 where절의 파라미터로 전달했다.
이렇게 하면 코드의 재사용성도 올라가고 가독성도 좋다.
BooleanExpression은 null 반환 시 자동으로 조건절에서 제거 된다. 단, 모든 조건이 null이 발생 시 전체 엔티티를 불러오게 되므로 대장애가 발생할 수 있다. 그러므로 null 값 처리를 해줘야한다.
참고로 JPA where절의 파라미터에서는 BooleanExpression 또는 Predicate[]을 쓸 수 있다.
🧷 참고. Predicate vs BooleanExpression 차이
BooleanExpression은 Predicate의 구현체다. 그러므로 where절에 BooleanExpression 타입을 파라미터로 전달할 수 있다.
BooleanExpression가 Predicate보다 더 선호되는 이유는 and와 or 같은 메서드를 통해 BooleanExpression을 조합해서 새로운 BooleanExpression을 만들 수 있다는 장점 때문이다. 그러므로 재사용성도 높다. 그리고BooleanExpression은 null 값을 반환할 경우 where절에서 조건이 무시되기 때문에 안전하다.
수정, 삭제 벌크 연산
벌크 연산은 쿼리 한 번으로 대량의 데이터를 수정 및 삭제할 때 사용한다.
벌크 연산은 영속성 컨텍스트를 거치지 않고 쿼리를 바로 DB에게 전달한다.
1. 쿼리 한 번으로 대량의 데이터 수정
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* 벌크 연산 수정
*/
@Test
void bulkUpdate() {
// member1 = 10 → 비회원
// member2 = 20 → 비회원
// member3 = 30 → member3 유지
// member4 = 40 → member4 유지
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(28))
.execute();
// 벌크 연산 후 영속성 컨텍스트 초기화
em.flush();
em.clear();
}
}
/* update
Member member1
set
member1.username = ?1
where
member1.age < ?2 */ update
member
set
username=?
where
age<?
🔍 벌크 연산 : 덧셈
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* 벌크 연산 : 덧셈
* 뺄셈할 때는 .add(-1)로 쓸 것
*/
@Test
void bulkAdd() {
long count = queryFactory
.update(member)
.set(member.age, member.age.add(1))
.execute();
// 벌크 연산 후 영속성 컨텍스트 초기화
em.flush();
em.clear();
}
}
/* update
Member member1
set
member1.age = member1.age + ?1 */ update
member
set
age=age+?
만약에 뺄셈을 원한다면 .add(-1)로 할 것
🔍 벌크 연산 : 곱셈
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* 벌크 연산 : 곱셈
*/
@Test
void bulkMultiply() {
long count = queryFactory
.update(member)
.set(member.age, member.age.multiply(2))
.execute();
// 벌크 연산 후 영속성 컨텍스트 초기화
em.flush();
em.clear();
}
}
/* update
Member member1
set
member1.age = member1.age * ?1 */ update
member
set
age=age*?
🔍 벌크 연산 : 나눗셈
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* 벌크 연산 : 나눗셈
*/
@Test
void bulkDivide() {
long count = queryFactory
.update(member)
.set(member.age, member.age.divide(3))
.execute();
// 벌크 연산 후 영속성 컨텍스트 초기화
em.flush();
em.clear();
}
}
/* update
Member member1
set
member1.age = member1.age / ?1 */ update
member
set
age=age/?
🔍 쿼리 한 번으로 대량 데이터 삭제
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* 벌크 연산 : 제거
*/
@Test
void bulkDelete() {
long count = queryFactory
.delete(member)
.where(member.age.gt(18))
.execute();
// 벌크 연산 후 영속성 컨텍스트 초기화
em.flush();
em.clear();
}
}
/* delete
from
Member member1
where
member1.age > ?1 */ delete
from
member
where
age>?
2. 벌크 연산시 주의 사항
벌크 연산은 JPQL 배치와 마찬가지로 영속성 컨텍스트에 있는 엔티티를 무시하고 바로 DB에 접근한다. 그렇기 때문에 배치 쿼리를 실행하고 나면 영속성 컨텍스트를 초기화하는 것이 안전하다.
또는 벌크 연산을 가장 먼저 실행하는 것도 괜찮다.
SQL function 호출하기
SQL function은 JPA와 같이 Dialect에 등록된 메서드만 호출할 수 있다.
🔍 replace 함수 사용
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
// 숫자인 경우 numberTemplate 사용할 것
/**
* Sql Function 사용
* - member.username에서 'member'를 'M'으로 변경
*/
@Test
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);
}
}
}
/* select
function('replace',
member1.username,
?1,
?2)
from
Member member1 */ select
replace(member0_.username,
?,
?) as col_0_0_
from
member member0_
s = M1
s = M2
s = M3
s = M4
숫자인 경우 Expressions.numberTemplate 사용할 것
🔍 소문자로 바꾸기 - lower()
SQL Function 기능을 기본적으로 querydsl도 제공한다.
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
}
/**
* SQL Function 사용
* - member.username을 소문자로 변경
*/
@Test
void sqlFunction2() {
List<String> result = queryFactory
.select(member.username)
.from(member)
.where(member.username.eq(Expressions.stringTemplate("function('lower', {0})", member.username)))
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
}
/* select
member1.username
from
Member member1
where
member1.username = function('lower', member1.username) */ select
member0_.username as col_0_0_
from
member member0_
where
member0_.username=lower(member0_.username)
s = member1
s = member2
s = member3
s = member4
대문자로 바꾸고 싶다면 upper()를 사용하면 된다.
👀 참고 자료
https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84/dashboard
http://querydsl.com/static/querydsl/3.4.1/reference/ko-KR/html/index.html
http://querydsl.com/static/querydsl/3.4.1/reference/ko-KR/html/ch03.html
https://dev-gorany.tistory.com/32
https://ict-nroo.tistory.com/117
'[JPA] > 실전! Querydsl' 카테고리의 다른 글
[Querydsl] 스프링 데이터 JPA가 제공하는 Querydsl 기능 (0) | 2022.04.29 |
---|---|
[Querydsl] 스프링 데이터 JPA 와 Querydsl (0) | 2022.04.28 |
[Querydsl] 실무 활용 - 순수 JPA와 Querydsl (0) | 2022.04.27 |
[Querydsl] 기본 문법 (0) | 2022.04.24 |
[QueryDSL] 프로젝트 환경 설정 (0) | 2022.04.24 |