https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84/
강의를 들으며 생각 정리
프로젝션과 결과 반환 - 기본
프로젝션: select 대상 지정
<프로젝션 대상이 하나>
@Test
public void simpleProjection() {
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있다.
프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회
<튜플 조회>
@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);
}
}
+) 참고
Tuple은 Querydsl에서 제공하는 타입이다.
결국 쿼리 기술인 Querydsl 소속이기 때문에 Tuple은 Service 계층에 노출시키기 보다는 리포지토리 안에서만 사용하는 것이 좋다.
Service 등 외부로 나갈 때는 DTO로 변환하는 것을 권장한다.
프로젝션과 결과 반환 - DTO 조회
DTO는 다음과 같다고 하자.
@Data
@NoArgsConstructor
public class MemberDto {
private String username;
private int age;
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
<순수 JPA에서 DTO 조회>
@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 이름을 다 적어줘야 해서 지저분하다.
생성자 방식만 지원한다.
Querydsl에서 DTO 반환 방법을 알아보자.
<1. 프로퍼티 접근>
@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);
}
}
Projections.bean: DTO의 Setter로 접근해서 DTO를 생성한다. 이 때, DTO의 기본 생성자가 있어야 한다.
<2. 필드 직접 접근>
@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);
}
}
Projections.fields: DTO의 필드에 직접 값을 대입한다.
프로퍼티, 필드 접근 생성 방식 모두 엔티티의 필드와, DTO의 필드명이 일치해야 한다.
만약 필드명이 다른 경우는 어떻게 할까? 여기 새로운 DTO가 있다.
@Data
@NoArgsConstructor
public class UserDto {
private String name;
private int age;
public UserDto(String name, int age) {
this.name = name;
this.age = age;
}
}
username이 아닌 name 사용
<별칭이 다를 때>
@Test
public void findUserDto() {
QMember memberSub = new QMember("memberSub");
List<UserDto> result = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"),
ExpressionUtils.as(
select(memberSub.age.max())
.from(memberSub), "age")
))
.from(member)
.fetch();
for (UserDto userDto : result) {
System.out.println("userDto = " + userDto);
}
}
member.username.as("name"): 필드에 별칭을 적용할 수 있다.
ExpressionUtils.as(source,alias): 필드나, 서브 쿼리에 별칭을 적용할 수 있다.
<3. 생성자 사용>
@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);
}
}
Projections.constructor: 생성자 사용 방식의 경우 엔티티와 DTO의 별칭이 달라도 타입이 맞는 생성자가 있다면 DTO 생성이 가능하다.
프로젝션과 결과 반환 - @QueryProjection
@QueryProjection을 사용하기 위해서는 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;
}
}
이후 compileQuerydsl을 하면 QMemberDto가 생성된다.
<@QueryProjection>
@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를 반환할 수 있다.
기존 생성자를 사용하는 방식과 어떤 차이? (Projections.constructor)
- Projections.constructor는 생성자 인자 값의 타입이나 개수가 다를 때, 런타임 오류로 잡을 수 있다.
- @QueryProjection은 생성자 인자 값이 타입이나 개수가 다를 때, 컴파일 오류로 바로 잡을 수 있다.
<단점>
1. DTO에 Querydsl 애노테이션을 유지하고, DTO까지 Q 파일을 생성해야 한다.
2. DTO가 Querydsl에 의존하게 된다.
-> Querydsl을 더 이상 사용하지 않는다면 DTO 역시 수정해야 한다.
-> DTO는 리포지토리가 아닌 다양한 레이어에서 사용할 수 있는데, Querydsl에 의존하는 DTO는 순수하지 않다.
동적 쿼리 - 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();
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();
}
builder에 조건들을 붙여서 where절 조건으로 사용할 수 있다.
동적 쿼리 - 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 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;
}
where문에 여러 조건을 추가한다. where 조건에 null 값은 무시된다.
이렇게 메서드로 관리할 때 좋은 점은 가독성이 높아지고 재사용할 수 있다는 것이다.
메서드의 반환 타입은 Predicate와 BooleanExpression 모두 가능하다)
이처럼 메서드를 통합할 수도 있다. (대신 null 관리를 잘 해주어야 한다)
private Predicate allEq(String usernameCond, Integer ageCond) {
return usernameEq(usernameCond).and(ageEq(ageCond));
}
수정, 삭제 배치 쿼리
<쿼리 한번으로 대량 데이터 수정>
@Test
public void bulkUpdate() {
//member1 = 10 -> 비회원
//member2 = 20 -> 비회원
//member3 = 30 -> 유지
//member4 = 40 -> 유지
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(28))
.execute();
}
+) 주의
벌크 연산은 DB에 직접 대량으로 update를 하기 때문에, DB와 영속성 컨텍스트 간에 차이가 있다.
만약 여기서 select(member)로 조회를 하게 된다면 영속성 컨텍스트가 우선이기 때문에 결국 수정된 DB의 member는 조회하지 못하게 된다.
따라서, 벌크 update 이후에 영속성 컨텍스트를 초기화 해주는 것이 중요하다.
@Test
public void bulkUpdate() {
//member1 = 10 -> 비회원
//member2 = 20 -> 비회원
//member3 = 30 -> 유지
//member4 = 40 -> 유지
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();
}
<기존 숫자에 1 더하기>
@Test
public void bulkAdd() {
long count = queryFactory
.update(member)
.set(member.age, member.age.add(1))
.execute();
}
곱하기 - multiply() 등 여러 연산을 지원한다.
<쿼리 한번으로 대량 데이터 삭제>
@Test
public void bulkDelete() {
long count = queryFactory
.delete(member)
.where(member.age.gt(18))
.execute();
}
SQL function 호출하기
SQL function은 현재 사용하는 DB의 Dialect에 등록된 내용만 호출할 수 있다.
<member -> M으로 변경하는 replace 함수 사용>
@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);
}
}
<소문자로 변경해서 비교 - lower 함수 사용>
@Test
public 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);
}
}
lower 같은 ansi 표준 함수들은 querydsl이 상당부분 내장하고 있다. 따라서 다음과 같이 처리해도 결과는 같다.
.where(member.username.eq(member.username.lower()))
'java > querydsl' 카테고리의 다른 글
[Querydsl] 실무 활용 - 스프링 데이터 JPA와 Querydsl (0) | 2021.06.06 |
---|---|
[Querydsl] 실무 활용 - 순수 JPA와 Querydsl (0) | 2021.06.04 |
[Querydsl] 기본 문법 (0) | 2021.06.02 |
[Querydsl] 예제 도메인 모델 (1) | 2021.06.01 |
[Querydsl] 프로젝트 환경설정 (0) | 2021.06.01 |