티스토리 뷰
모든 소스 코드는 여기 있습니다.
이번엔 Querydsl을 이용해 벌크 쿼리를 작성해보겠습니다.
벌크 Update
쿼리를 바로 수정하기 전에 Player의 시즌/비시즌 상태를 나타내는 Boolean 타입 변수 inSeason과 주급을 나타내는 Integer 타입 변수 weeklySalary를 추가해보겠습니다.
package io.lcalmsky.querydsl.domain;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import javax.persistence.*;
import java.util.Optional;
@Table(name = "Player")
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class Player {
@Id
@GeneratedValue
@Column(name = "player_id")
private Long id;
private String name;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
@ToString.Exclude
private Team team;
private Boolean inSeason;
private Integer weeklySalary;
public Player(String name) {
this(name, 0, null);
}
public Player(String name, int age) {
this(name, age, null);
}
public Player(String name, int age, Team team) {
this.name = name;
this.age = age;
if (team != null) {
changeTeam(team);
}
this.inSeason = false;
this.weeklySalary = 0;
}
private void changeTeam(Team team) {
Optional.ofNullable(this.team).ifPresent(t -> t.removeIfExist(this));
this.team = team;
team.getPlayers().add(this);
}
public void begins() {
this.inSeason = true;
}
public void over() {
this.inSeason = false;
}
public void contactSalary(int weeklySalary) {
this.weeklySalary = weeklySalary;
}
public void raiseSalary(float rate) {
this.weeklySalary = (int) (weeklySalary + weeklySalary * rate);
}
}
Entity를 수정했으면 compileQuerydsl을 실행해줘야겠죠?
> ./gradlew compileQuerydsl
혹시
compileQuerydsl실행 시 에러가 발생한다면clean이후 다시 진행해보세요.> ./gradlew clean
실행하고 나면 QPlayer 클래스에 시즌 상태를 나타내는 필드가 추가됩니다.
리그가 끝나면 모두 시즌 off 상태가 되기 때문에 한 번에 바꿔주는 쿼리를 작성할 예정입니다.
바로 태스트 코드를 작성해보겠습니다.
package io.lcalmsky.querydsl.domain;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.persistence.EntityManager;
import org.springframework.transaction.annotation.Transactional;;
import java.util.Arrays;
import java.util.List;
import static io.lcalmsky.querydsl.domain.QPlayer.player;
@SpringBootTest
@Transactional
class PlayerTest {
@Autowired
EntityManager entityManager;
private JPAQueryFactory queryFactory;
@BeforeEach
void setup() {
Team tottenhamHotspur = new Team("Tottenham Hotspur F.C.");
Team manchesterCity = new Team("Manchester City F.C.");
entityManager.persist(tottenhamHotspur);
entityManager.persist(manchesterCity);
Player harryKane = new Player("Harry Kane", 27, tottenhamHotspur);
harryKane.contactSalary(200000); // (1)
harryKane.begins(); // (1)
Player heungminSon = new Player("Heungmin Son", 29, tottenhamHotspur);
heungminSon.contactSalary(140000); // (1)
heungminSon.begins(); // (1)
Player kevinDeBruyne = new Player("Kevin De Bruyne", 30, manchesterCity);
kevinDeBruyne.contactSalary(350000); // (1)
kevinDeBruyne.begins(); // (1)
Player raheemSterling = new Player("Raheem Shaquille Sterling", 26, manchesterCity);
raheemSterling.contactSalary(300000); // (1)
raheemSterling.begins(); // (1)
entityManager.persist(harryKane);
entityManager.persist(heungminSon);
entityManager.persist(kevinDeBruyne);
entityManager.persist(raheemSterling);
queryFactory = new JPAQueryFactory(entityManager);
}
@Test
void simpleQuerydslWithBulkUpdate() {
// when
long affectedRows = queryFactory // (2)
.update(player) // (3)
.set(player.inSeason, false)
.execute(); // (4)
entityManager.flush();
entityManager.clear();
// then
List<Boolean> actual = queryFactory
.select(player.inSeason)
.from(player)
.fetch();
assertEquals(Arrays.asList(false, false, false, false), actual);
}
}
(1) 시즌 상태와 주급 필드가 추가되었기 때문에
Entity생성시 초기화 하는 부분을 추가하였습니다.
(2)update-execute의 반환 값은 영향 받은row의 수이고 타입은long입니다.
(3)update메서드를 사용합니다.
(4)execute를 호출해 쿼리를 실행시킵니다.
이제 테스트를 실행해볼까요?
2021-07-23 22:24:21.986 DEBUG 13849 --- [ main] org.hibernate.SQL :
/* update
Player player
set
player.inSeason = ?1 */ update
player
set
in_season=?
2021-07-23 22:24:22.012 DEBUG 13849 --- [ main] org.hibernate.SQL :
/* select
player.inSeason
from
Player player */ select
player0_.in_season as col_0_0_
from
player player0_
테스트는 성공했고 마지막에 update 하는 부분과 검증을 위해 select하는 부분의 쿼리입니다.
매우 익숙한(?) SQL 업데이트를 그대로 사용하시면 됩니다.
다음으로 한 단계 더 응용해서 주급이 20만 유로 이하인 선수들의 주급을 10만 유로씩 상승시켜보겠습니다.
@Test
void simpleQuerydslWithBulkUpdate2(){
// when
long affectedRows=queryFactory
.update(player)
.set(player.weeklySalary,player.weeklySalary.add(100000)) // (1)
.where(player.weeklySalary.loe(200000))
.execute();
entityManager.flush();
entityManager.clear();
// then
List<Player> players=queryFactory.selectFrom(player)
.fetch();
assertEquals(2,affectedRows);
players.forEach(System.out::println);
}
(1)
add를 사용해 10만 유로만큼 더해줬습니다.
2021-07-23 23:10:13.354 DEBUG 14296 --- [ main] org.hibernate.SQL :
/* update
Player player
set
player.weeklySalary = player.weeklySalary + ?1
where
player.weeklySalary <= ?2 */ update
player
set
weekly_salary=weekly_salary+?
where
weekly_salary<=?
2021-07-23 23:10:13.385 DEBUG 14296 --- [ main] org.hibernate.SQL :
/* select
player
from
Player player */ select
player0_.player_id as player_i1_1_,
player0_.age as age2_1_,
player0_.in_season as in_seaso3_1_,
player0_.name as name4_1_,
player0_.team_id as team_id6_1_,
player0_.weekly_salary as weekly_s5_1_
from
player player0_
Player(id=3, name=Harry Kane, age=27, inSeason=true, weeklySalary=300000)
Player(id=4, name=Heungmin Son, age=29, inSeason=true, weeklySalary=240000)
Player(id=5, name=Kevin De Bruyne, age=30, inSeason=true, weeklySalary=350000)
Player(id=6, name=Raheem Shaquille Sterling, age=26, inSeason=true, weeklySalary=300000)
정상적으로 수행된 것을 확인할 수 있습니다.
주급을 정액으로 일괄 상승하는 것이 아니라 20% 씩 증가시킨다면 어떻게 해야할까요?
@Test
void simpleQuerydslWithBulkUpdate3(){
// when
long affectedRows=queryFactory
.update(player)
.set(player.weeklySalary,player.weeklySalary.multiply(1.2)) // (1)
.where(player.weeklySalary.loe(200000))
.execute();
entityManager.flush();
entityManager.clear();
// then
List<Player> players=queryFactory.selectFrom(player)
.fetch();
assertEquals(2,affectedRows);
players.forEach(System.out::println);
}
(1)
add대신multiply를 사용하였고 20% 상승을 위해 1.2를 곱해줬습니다.
수행 결과는...
2021-07-23 23:11:31.221 DEBUG 14305 --- [ main] org.hibernate.SQL :
/* update
Player player
set
player.weeklySalary = player.weeklySalary * ?1
where
player.weeklySalary <= ?2 */ update
player
set
weekly_salary=weekly_salary*?
where
weekly_salary<=?
2021-07-23 23:11:31.250 DEBUG 14305 --- [ main] org.hibernate.SQL :
/* select
player
from
Player player */ select
player0_.player_id as player_i1_1_,
player0_.age as age2_1_,
player0_.in_season as in_seaso3_1_,
player0_.name as name4_1_,
player0_.team_id as team_id6_1_,
player0_.weekly_salary as weekly_s5_1_
from
player player0_
Player(id=3, name=Harry Kane, age=27, inSeason=true, weeklySalary=200000)
Player(id=4, name=Heungmin Son, age=29, inSeason=true, weeklySalary=140000)
Player(id=5, name=Kevin De Bruyne, age=30, inSeason=true, weeklySalary=350000)
Player(id=6, name=Raheem Shaquille Sterling, age=26, inSeason=true, weeklySalary=300000)
오잉? 🤔 값이 바뀌지 않았습니다. flush와 clear를 호출했기 때문에 영속성 컨텍스트에 남아있어서도 아니고 원인이 뭔지 한참 헤맸습니다.
그 이유는 바로 multiply 메서드를 추적해보면 알 수 있습니다.
멀리 갈 필요도 없이 NumberExpression 클래스만 확인해보면 되는데요,
public<N extends Number & Comparable<N>> NumberExpression<T> multiply(N right){
return Expressions.numberOperation(getType(),Ops.MULT,mixin,ConstantImpl.create(right));
}
multiply 메서드는 Number의 자식클래스 Generic 타입을 파라미터로 받고있습니다.
따라서 앞서 player.weeklySalary를 Integer로 선언했기 때문에 1.2를 넣더라도 자동으로 int로 캐스팅되어 1을 곱하게 됩니다.
해결하기 위해서는 weeklySalary 필드를 소숫점 사용 가능한 타입으로 변경하든지, 아니면 같은 Integer 타입을 곱해주면 됩니다.
전자의 경우 다시 compileQuerydsl을 수행해야하기 때문에 번거로워 화끈하게 주급을 두 배로 인상해주기로 하였습니다.
@Test
void simpleQuerydslWithBulkUpdate3(){
// when
long affectedRows=queryFactory
.update(player)
.set(player.weeklySalary,player.weeklySalary.multiply(2)) // (1)
.where(player.weeklySalary.loe(200000))
.execute();
entityManager.flush();
entityManager.clear();
// then
List<Player> players=queryFactory.selectFrom(player)
.fetch();
assertEquals(2,affectedRows);
players.forEach(System.out::println);
}
(1)
multiply메서드에weeklySalary와 같은 정수 타입인 2를 전달하였습니다.
Player(id=3, name=Harry Kane, age=27, inSeason=true, weeklySalary=400000)
Player(id=4, name=Heungmin Son, age=29, inSeason=true, weeklySalary=280000)
Player(id=5, name=Kevin De Bruyne, age=30, inSeason=true, weeklySalary=350000)
Player(id=6, name=Raheem Shaquille Sterling, age=26, inSeason=true, weeklySalary=300000)
쿼리는 생략하고 출력한 결과만 봤을 때는 정상적으로 두 배 인상된 주급을 확인할 수 있습니다.
사실 저는 후자로도 테스트 해봤고 정상적으로 반영된 것을 확인했습니다.
다들 직접 확인해보세요 😁
벌크 Delete
다음은 벌크로 데이터를 삭제하는 방법입니다.
마찬가지로 엄청 간단하기 때문에 테스트 코드로 바로 확인해보겠습니다.
@Test
void simpleQuerydslWithBulkDelete(){
// when
long affectedRows=queryFactory
.delete(player) // (1)
.where(player.weeklySalary.goe(200000))
.execute();
// then
entityManager.flush();
entityManager.clear();
assertEquals(affectedRows,3); // (2)
List<Player> players=queryFactory
.selectFrom(player)
.fetch();
assertEquals(1,players.size()); // (3)
System.out.println("players = "+players);
}
(1)
update대신delete를 사용하고 나머지는 동일합니다.
(2) 주급이 20만 유로 이상인 선수는 세 명 이므로 3개의 row가 영향을 받습니다.
(3) 세 명의 선수가 DB에서 지워졌기 때문에 한 명의 선수만 검색되어야 합니다.
2021-07-23 23:28:30.455 DEBUG 14468 --- [ main] org.hibernate.SQL :
/* delete
from
Player player
where
player.weeklySalary >= ?1 */ delete
from
player
where
weekly_salary>=?
2021-07-23 23:28:30.488 DEBUG 14468 --- [ main] org.hibernate.SQL :
/* select
player
from
Player player */ select
player0_.player_id as player_i1_1_,
player0_.age as age2_1_,
player0_.in_season as in_seaso3_1_,
player0_.name as name4_1_,
player0_.team_id as team_id6_1_,
player0_.weekly_salary as weekly_s5_1_
from
player player0_
players = [Player(id=4, name=Heungmin Son, age=29, inSeason=true, weeklySalary=140000)]
테스트 결과 슬프게도 손흥민 선수만 남았습니다. 😢
JPA나 스프링 데이터 JPA로 벌크 쿼리를 수행할 때도 많이 복잡한 건 아니었지만 Querydsl을 사용하는 것이 훨씬 더 간단한 느낌을 받았습니다.
동적으로 조건이 변하는 상황이라면 더욱 더 적극적으로 사용할 수 있겠네요.
다음 포스팅에서는 SQL Function을 사용하는 부분을 다뤄보겠습니다. 🙋
이번 포스팅을 작성하다가 깨달은 것인데.. 그동안
org.springframework.transaction.annotation.Transactional대신javax.transaction.Transactional를 사용하고 있었더군요 ㅜㅜ 검증보다는 결과를 출력하는 방식으로 테스트 클래스를 작성하다보니 뭐가 잘못됐는지도 한참동안 모르고 있었네요.. 테스트 코드를 제대로 작성하는 것의 소중함을 이렇게 또 깨달았습니다.😥 후딱 기존 포스팅도 다 수정해놓아야겠네요 🏃
'Querydsl' 카테고리의 다른 글
| [Querydsl] Spring Data JPA와 같이 사용하기 (0) | 2021.07.26 |
|---|---|
| [Querydsl] SQL 함수 사용하기 (0) | 2021.07.25 |
| [Querydsl] 동적 쿼리 (1) | 2021.07.23 |
| [Querydsl] Projection & 결과 매핑 (0) | 2021.07.22 |
| [Querydsl] 서브 쿼리(Subquery), Case(when, then), 상수(Constant), concat (0) | 2021.07.21 |
- Total
- Today
- Yesterday
- 스프링부트
- leetcode
- 함께 자라기
- Spring Boot
- gRPC
- r
- 클린 아키텍처
- intellij
- 알고리즘
- 헥사고날 아키텍처
- QueryDSL
- 스프링 부트
- 함께 자라기 후기
- 스프링 데이터 jpa
- JSON
- Spring Boot JPA
- spring boot jwt
- Java
- @ManyToOne
- Spring Boot Tutorial
- 스프링 부트 회원 가입
- spring boot application
- Spring Data JPA
- 스프링 부트 애플리케이션
- Linux
- Jackson
- proto3
- 스프링 부트 튜토리얼
- spring boot app
- JPA
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
