중급 문법
프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있음
프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회
- 프로젝션: select 대상 지정
프로젝션과 결과 반환 - 기본
@Data
public class MemberDto {
private String username;
private int age;
public MemberDto() {
}
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
} }
@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("age = " + age);
System.out.println("username = " + username);
}
}
// 튜플을 리포지토리 계층에서 쓰는 건 괜찮은데 서비스나 컨트롤러 계층으로 넘어가는건 좋지 않다.
// querydsl 에서 다른 기술로 바꿀 때에도 앞단(컨트롤러나 서비스)을 바꿀 필요가 없어진다. 튜플은 querydsl의 종속적인 타입이기 때문이다.
// 바깥 계층으로 가져갈 때는 DTO를 사용하자
프로젝션과 결과 반환 - DTO 조회
순수 JPA에서 DTO 조회 코드
@Test
public void findDtoByJPQL(){ // jpql 의 new operation 문법, 생성자 방식만 지원함
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);
}
}
Querydsl 빈 생성(Bean population)
결과를 DTO 반환할 때 사용
다음 3가지 방법 지원
- 프로퍼티 접근
- 필드 직접 접근
- 생성자 사용
@Test
public void findDtoBySetter(){ // memberDto 를 기본 생성자로 생성한 후에 값을 세팅 해야 하기 때문에 dto에는 기본 생성자가 필수로 있어야 한다.
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age)) // setter
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
@Test
public void findDtoByField(){ // getter setter 무시하고 바로 필드에
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);
}
}
별칭이 다를 때
package study.querydsl.dto;
import lombok.Data;
@Data
public class UserDto {
private String name;
private int age;
}
@Test
public void findUserDto(){
QMember memberSub = new QMember("memberSub");
List<UserDto> result = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"),
ExpressionUtils.as(JPAExpressions // 서브 쿼리 (모든 나이를 max나이로 출력) - 서브쿼리의 결과가 age 에 매칭
.select(memberSub.age.max())
.from(memberSub), "age")
))
.from(member)
.fetch();
for (UserDto userDto : result) {
System.out.println("userDto = " + userDto);
}
}
프로퍼티나 필드 접근 생성 방식에서 이름이 다를 때
ExpressionUtils.as(source, alias) : 필드나 서브 쿼리에 별칭 적용
username.as(”name”) : 필드에 별칭 적용
프로젝션과 결과 반환 - @QueryProjection
생성자를 사용하는 방식에서는 Query Projection 방식까지 지원을 해준다.
Dto 를 Q 파일로 생성할 수 있다.
@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를 생성자에 추가한다.
./gradlew compileJava
QMemberdto 생성 확인
@Test
public void findDtoByQueryProjection(){
// Projections.constructor 는 타입이 안맞으면 런타임 오류가 발생한다.
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age)) // compile 시점에 타입이 안맞으면 오류
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
단점은 DTO 가 querydsl을 의존해야 한다는 점이다.
동적 쿼리 - BooleanBuilder 사용
@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();
//BooleanBuilder builder = new BooleanBuilder(member.username.eq(usernameCond)); // 무조건 username이 있다고 가정해서 초기 값을 넣을 수도 있다.
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();
}
동적 쿼리 - where 다중 파라미터 사용
더 직관적이므로 사용을 권장한다.
@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))
.fetch();
}
private Predicate usernameEq(String usernameCond) {
return usernameCond != null ? member.username.eq(usernameCond) : null; // where 절에 null 이 들어오면 무시된다.
}
private Predicate ageEq(Integer ageCond) {
return ageCond != null ? member.age.eq(ageCond) : null;
}
아래와 같이 조립할 수도 있다.
private BooleanExpression allEq(String usernameCond, Integer ageCond){
return usernameEq(usernameCond).and(ageEq(ageCond));
}
private List<Member> searchMember2(String usernameCond, Integer ageCond) {
return queryFactory
.selectFrom(member)
//.where(usernameEq(usernameCond), ageEq(ageCond))
.where(allEq(usernameCond, ageCond))
.fetch();
}
where 절의 null 값은 무시된다.
메서드를 여러 군데의 where 절에서 재사용 할 수 있다는 장점이 있다.
쿼리의 가독성이 높아진다.
조합이 가능해진다. ex) isServiceable()
수정, 삭제 벌크 연산
@Test
public void bulkUpdate(){
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(28))
.execute(); // 영향을 받은 로우 수
em.flush();
em.clear();
List<Member> result = queryFactory
.selectFrom(member)
.fetch();
for (Member member1 : result) {
System.out.println("member1 = " + member1);
}
}
bulk 연산은 영속성 컨텍스트를 무시하고 db에 바로 쿼리가 나간다.
db 의 상태와 영속성 컨텍스트의 상태가 달라져 버린다.
영속성 컨텍스트는 그대로이고 db 는 수정된 상태 이상태에서 select로 멤버를 가져오면 영속성 컨텍스트에 같은 id 값이 있다면 DB 에서 가져온걸 버린다. 영속성 컨텍스트가 유지된다. 영속성 컨텍스트가 항상 우선권을 가진다.
따라서 em.flush(); em.clear();를 해주자
@Test
public void bulkAdd(){
long count = queryFactory
.update(member)
.set(member.age, member.age.add(1))
.execute();
}
@Test
public void bulkDelete(){
long count = queryFactory
.delete(member)
.where(member.age.gt(18))
.execute();
}
SQL function 호출하기
SQL function은 JPA와 같이 Dialect에 등록된 내용만 호출할 수 있다.
@Test
public void sqlFunction(){ // member M으로 변경하는 replace 함수 사용
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
// (Expressions.stringTemplate("function('lower', {0})", member.username)))
.where(member.username.eq(member.username.lower()))
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
// lower 같은 ansi 표준 함수들은 querydsl이 상당부분 내장하고 있다.
인프런 - 실전! Querydsl
'BACK > JPA' 카테고리의 다른 글
[JPA] 실무 활용 - 스프링 데이터 JPA 와 Querydsl (1) | 2024.03.15 |
---|---|
[JPA] 실무 활용 - 순수 JPA 와 Querydsl (0) | 2024.03.15 |
[JPA] Querydsl 기본 문법 (검색, 조회, 정렬, 페이징, 집합, 조인등) (0) | 2024.03.15 |
[JPA] Querydsl 프로젝트 환경설정 - 설정과 검증 (0) | 2024.03.15 |
[JPA] Dirty Checking(변경 감지) (0) | 2023.04.11 |