JPQL - 경로 표현식
경로 표현식이란 .(점)을 찍어 객체 그래프를 탐색하는 것이다.
select m.username -> 상태 필드
from Member m
join m.team t -> 단일 값 연관 필드
join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A'
상태 필드로 가냐 단일값 연관필드로 가냐 컬렉션값 연관필드로 가냐에 따라서 내부적으로 동작하는 방식이 달라 결과가 달라진다. 그래서 이 세 가지를 꼭 구분해서 이해를 해야 된다.
경로 표현식 용어 정리
상태 필드(state field): 단순히 값을 저장하기 위한 필드(ex: m.username)
연관 필드(association field): 연관관계를 나타내기 위한 필드
연관 필드는 두 가지로 나눠진다.
- 단일 값 연관 필드:
- @ManyToOne, @OneToOne, 대상이 엔티티(ex: m.team)
- 컬렉션 값 연관 필드:
- @OneToMany, @ManyToMany, 대상이 컬렉션(ex: m.orders)
경로 표현식 특징
- 상태 필드(state field): 경로 탐색의 끝이다. 그래서 더 이상 탐색이 안된다.
- 예시 JPQL) select m.username From Member m
- 위 쿼리에서 m.username은 경로 탐색의 끝이다. m.username. 해서 더 탐색을 할 수 없다.
- 단일 값 연관 경로: 묵시적 내부 조인(inner join) 발생, 탐색을 더 할 수 있다.
- 예시 JPQL) select m.team From Member m
- m.team.name 이런 식으로 m.team에서 더 탐색하는 게 가능하다.
- m.team 이 의미는 member와 연관된 team을 가져오겠다는 의미다.
- 객체 입장에서는 .(점)을 찍어 참조해서 값을 찾으면 되지만 DB입장에서는 조인을 해야 데이터를 가져올 수 있다.
- 그래서 묵시적으로 조인이 발생한다. 이런 부분은 쿼리 튜닝하기 어렵다.
- 웬만하면 묵시적인 내부 조인이 발생하게 짜면 안 된다.
- 실제 운영할 때는 테이블과 쿼리가 굉장히 많다. 그래서 조인 같은 것들은 성능 튜닝에 많은 영향을 준다.
- 웬만하면 JPQL과 SQL을 비슷하게 맞추는 게 좋다.
- 컬렉션 값 연관 경로: 묵시적 내부 조인 발생, 탐색을 못한다.
- 예시 JPQL) select t.members From Team t
- t.members 에서 탐색이 끝난다. t.members. 이런 식으로 더 탐색이 불가능하다.
- t.members.size 쓸 수 있는 건 size 정도이다.
- FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능
결론: 묵시적 조인을 사용하지 말고 명시적 조인을 사용하자.
명시적 조인을 써야 쿼리 튜닝하기도 쉽다 JPQL을 튜닝하면 쿼리가 튜닝이 된다.
- 상태 필드 경로 탐색
- JPQL: select m.username, m.age from Member m
- SQL: select m.username, m.age from Member m
- 단일 값 연관 경로 탐색
- JPQL: select o.member from Order o
- SQL
select m.*
from Orders o
inner join Member m on o.member_id = m.id
-- 주의! 묵시적 조인 발생!
명시직 조인, 묵시적 조인
명시적 조인: join 키워드 직접 사용
select m from Member m join m.team t
묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생(내부 조인만 가능)
select m.team from Member m
경로 표현식 - 예제
- select o.member.team from Order o -> 성공
- select t.members from Team -> 성공
- select t.members.username from Team t -> 실패
- select m.username from Team t join t.members m -> 성공
- t.members 해서 컬렉션을 묵시적 조인이 아닌 명시적 조인으로 해서 가져온 다음에 별칭 m을 가져와서 username으로 했기 때문에 m에서 다시 시작한 거라 가능하다.
경로 탐색을 사용한 묵시적 조인 시 주의사항
- 항상 내부 조인
- 컬렉션은 경로 탐색의 끝이셔서 명시적 조인을 통해 별칭을 얻어야 탐색을 이어갈 수 있다.
- 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM (JOIN) 절에 영향을 준다.
실무 조언
- 가급적 묵시적 조인 대신에 명시적 조인 사용
- 조인은 SQL 튜닝에 중요 포인트
- 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움
JPQL - 페치 조인(fetch join)
실무에서 정말정말 중요함
- SQL 조인 종류가 아니다.
- JPQL에서 성능 최적화를 위해 제공하는 기능이다.
- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능이다.
- join fetch 명령어 사용한다.
- 페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
엔티티 페치 조인
회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에)하고 싶을 때 사용
JPQL에서는 select m 이라고 하나만 적었는데 실행된 SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT 한다.
[JPQL]
select m from Member m join fetch m.team
[SQL]
SELECT M.*, T.* FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID=T.ID
즉시로딩과 비슷해 보이나 페치 조인은 원하는 대로 객체 그래프를 한 번에 조회한다는 걸 직접 명시적으로 동적인 타이밍에 정할 수 있다.
위 예제를 살펴보자.
회원 1,2는 팀A 회원3은 팀B에 소속되어 있다.
회원 1,2,3을 조회하면서 각 회원이 속한 팀도 조회를 하고 싶다고 하자.
예시 코드
Team team1 = new Team();
team1.setName("teamA");
em.persist(team1);
Team team2 = new Team();
team2.setName("teamB");
em.persist(team2);
Team team3 = new Team();
team3.setName("teamC");
em.persist(team3);
Member member1 = new Member();
member1.setUsername("member1");
member1.addTeam(team1);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.addTeam(team1);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("member3");
member3.addTeam(team2);
em.persist(member3);
em.flush();
em.clear();
String query = "select m From Member m";
List<Member> resultList = em.createQuery(query, Member.class)
.getResultList();
for (Member member : resultList) {
System.out.println("member: " + member.getUsername() + ", " + member.getTeam().getName());
// 회원1, 팀A(SQL)
// 회원2, 팀A(1차캐시)
// 회원3, 팀B(SQL)
}
실행쿼리
Hibernate:
/* select
m
From
Member m */ select
m1_0.id,
m1_0.age,
m1_0.memberType,
m1_0.TEAM_ID,
m1_0.username
from
Member m1_0
Hibernate:
select
t1_0.id,
t1_0.name
from
Team t1_0
where
t1_0.id=?
member: member1, teamA
member: member2, teamA
Hibernate:
select
t1_0.id,
t1_0.name
from
Team t1_0
where
t1_0.id=?
member: member3, teamB
N + 1 문제 발생(즉시로딩이든, 지원로딩이든 다 발생함)
그래서 페치 조인을 사용해야 한다.
아래와 같이 페치 조인을 사용하는 JPQL로 수정하고 실행 쿼리를 봐보자.
String query = "select m From Member m join fetch m.team";
실행쿼리
Hibernate:
/* select
m
From
Member m
join
fetch
m.team */ select
m1_0.id,
m1_0.age,
m1_0.memberType,
t1_0.id,
t1_0.name,
m1_0.username
from
Member m1_0
join
Team t1_0
on t1_0.id=m1_0.TEAM_ID
join으로 select 쿼리가 한번만 발생했다. 그리고 select 문을 보면 member와 team 데이터를 다 가져왔다.
페치 조인을 사용해서 em.createQuery(query, Member.class) 시점에 이미 Team 데이터도 다 가져왔다. 그래서 반복문에서 member.getTeam() 이 부분에서 team은 프록시 객체가 아니라 영속성 컨텍스트에 저장되어 있는 team 엔티티 객체다.
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
.getResultList();
for (Member member : members) {
//페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩X
System.out.println("username = " + member.getUsername() + ", " +
"teamName = " + member.getTeam().name());
}
컬렉션 페치 조인
일대다 관계, 컬렉션 페치 조인
[JPQL]
select t
from Team t join fetch t.members
where t.name = ‘팀A'
[SQL]
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
예시 코드
String query = "select t From Team t join fetch t.members";
List<Team> resultList = em.createQuery(query, Team.class)
.getResultList();
for (Team team : resultList) {
System.out.println("team: " + team.getName() + ", members = " + team.getMembers().size());
}
실행쿼리
Hibernate:
/* select
t
From
Team t
join
fetch
t.members */ select
t1_0.id,
m1_0.TEAM_ID,
m1_0.id,
m1_0.age,
m1_0.memberType,
m1_0.username,
t1_0.name
from
Team t1_0
join
Member m1_0
on t1_0.id=m1_0.TEAM_ID
team: teamA, members = 2
team: teamB, members = 1
1 : 다 관계에서의 컬렉션 페치 조인 사용 시 데이터가 뻥튀기될 수 있다.
그래서 위 실행에서는 예전에 아래와 같이 데이터가 뻥튀기 되었다.
team: teamA, members = 2
team: teamA, members = 2
team: teamB, members = 1
하지만 하이버네이트6 변경사항에서 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용돼서 현재 2개만 나왔다.
데이터가 뻥튀기 예시
컬렉션 페치 조인 사용 코드 한번 더 정리해보자.
String jpql = "select t from Team t join fetch t.members where t.name = '팀A'"
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();
for(Team team : teams) {
System.out.println("teamname = " + team.getName() + ", team = " + team);
for (Member member : team.getMembers()) {
//페치 조인으로 팀과 회원을 함께 조회해서 지연 로딩 발생 안함
System.out.println(“-> username = " + member.getUsername()+ ", member = " + member);
}
}
출력 결과
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
페치 조인과 DISTINCT
SQL의 DISTINCT는 중복된 결과를 제거하는 명령이다.
근데 SQL의 DISTINCT는 중복을 다 제거할 수 없다.
그래서 JPQL의 DISTINCT 2가지 기능 제공한다.
- SQL에 DISTINCT를 추가
- 애플리케이션에서 엔티티 중복 제거
- SQL에 DISTINCT를 추가하여 DB에 쿼리를 전송 후 애플리케이션으로 결과를 가져왔을 때 똑같은 엔티티가 있으면 없애 준다.
select distinct t
from Team t join fetch t.members
where t.name = ‘팀A’
SQL에 DISTINCT는 row가 완전히 같아야 중복을 제거한다.
그래서 위 경우에서는 SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과에서 중복제거가 실패한다.
즉 SQL의 DISTINCT만으로는 데이터가 안 줄어든다.
그래서 JPA에서 DISTINC가 추가로 애플리케이션에서 중복 제거를 시도한다.
그러면서 같은 식별자를 가진 Team 엔티티를 제거한다.
[DISTINCT 추가 시]
teamname = 팀A, team = Team@0x100
-> username = 회원1, member = Member@0x200
-> username = 회원2, member = Member@0x300
하이버네이트6 변경 사항
DISTINCT가 추가로 애플리케이션에서 중복 제거시도
-> 하이버네이트6 부터는 DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용된다.
참고 링크
https://www.inflearn.com/questions/717679
페치 조인과 일반 조인의 차이
일반 조인 실행시 연관된 엔티티를 함께 조회하지 않는다.
그래서 아래 예시를 보면 team과 member을 조인했는데 가져온 데이터는 Team 데이터만 가져왔다.
그래서 member 데이터를 실제 사용 시 추가 쿼리가 발생한다.
[JPQL]
select t
from Team t join t.members m
where t.name = ‘팀A'
[SQL]
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'
정리
JPQL은 결과를 반환할 때 연관관계 고려하지 않는다.
단지 SELECT 절에 지정한 엔티티만 조회한다.
위 예시에서는 팀 엔티티만 조회하고, 회원 엔티티는 조회하지 않는다.
페치 조인을 사용할 때만 연관된 엔티티도 함께 조회한다.(즉시 로딩이 발생한다고 보면 된다.)
페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념이다.
한방 쿼리로 객체 그래프 필요한 걸 묶어 가지고 쫙 가지고 온다.
이 페치 조인을 사용하면 n + 1 문제는 웬만하면 다 해결된다.
페치 조인의 특징과 한계
페치 조인 대상에는 별칭을 줄 수 없다.
select t From Team t join fetch t.members as m
위 JPQL에서 as m처럼 별칭을 줄 수 없다.
하이버네이트는 가능, 가급적 사용X
- 예를 들어 team 데이터를 가져오면서 조인되는 member가 5명인데 이중 3명만 조인을 하고 싶다고 가정하자.
- 그래서 이 3명만 가져오기 위해 따로 조건을 둬서 가져오는 건 위험하다.
- 기본적으로 JPA의 설계 사상 자체가 객체 그래프는 기본적으로 데이터를 다 조회해야 된다이다.
- 객체 그래프를 탐색한다는 건 team에서 members를 탐색하면 members가 다 나온다는 걸 가정하고 설계가 되어 있다는 소리다.
- 그런데 3개만 가져왔는데 cascade 같은 이상 옵션들이 설정되어 있으면 잘못하면 나머지가 지워지거나 이상하게 동작할 수 있다.
- 그리고 어떤 부분에서는 member를 5명 다 조회하고 어떤 부분에서는 member를 3명만 조회하면 영속성 컨텍스트에서는 이런 부분을 처리하기 난감하다.
- 그래서 페치 조인 대상에는 별칭을 쓰지 말자!
둘 이상의 컬렉션은 페치 조인 할 수 없다.
- 패치 조인 컬렉션은 딱 하나만 지정할 수 있다.
컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
- 일대다는 페치 조인 사용 시 데이터 뻥튀기가 발생한다.
- 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
select m From Member m join fetch m.team t
위처럼 member에서 team으로 가는 건 다대일이다.
이런 식으로 방향을 뒤집어서 해결할 수 있다.
또는 아래와 같이 batch size를 사용해서 해결할 수 있다.
@BatchSize(size = 100)
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
페치 조인의 특징과 한계
- 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
- 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
- @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략
- 실무에서 글로벌 로딩 전략은 모두 지연 로딩
- 최적화가 필요한 곳은 페치 조인 적용
- N + 1 문제가 발생하는 곳에 주로 사용
페치 조인 - 정리
- 모든 것을 페치 조인으로 해결할 수는 없음
- 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
- member.team 이런 식으로 찾아가야 될 때는 효과적이다.
- 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
1. 페치 조인을 사용하여 엔티티를 가져와서 그걸 그대로 사용한다.
2. 패치 조인을 사용해서 엔티티를 가져온 후 애플리케이션에서 DTO로 바꿔서 화면을 반환한다.
3. JPQL을 처음부터 짤 때부터 new DTO로 딱 스위칭해서 가져온다.
다형성 쿼리
TYPE
조회 대상을 특정 자식으로 한정
예) Item 중에 Book, Movie를 조회해라
[JPQL]
select i from Item i
where type(i) IN (Book, Movie)
[SQL]
select i from i
where i.DTYPE in (‘B’, ‘M’)
TREAT(JPA 2.1)
- 자바의 타입 캐스팅과 유사
- 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
- FROM, WHERE, SELECT(하이버네이트 지원) 사용
TREAT(JPA 2.1)
예) 부모인 Item과 자식 Book이 있다.
[JPQL]
select i from Item i
where treat(i as Book).author = ‘kim’
[SQL]
select i.* from Item i
where i.DTYPE = ‘B’ and i.author = ‘kim’
엔티티 직접 사용
JPQL에서 엔티티를 직접 사용하면 어떻게 되느냐
엔티티 직접 사용 - 기본 키 값
JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.
SQL에서는 보통은 함수 안에다가 데이터나 식별자 값을 넘기지 count(m)처럼 엔티티 자체를 넘기지 않는다.
JPQL은 엔티티라는 걸 쓸 수 있기 때문에 count(m)이 가능하다.
[JPQL]
select count(m.id) from Member m //엔티티의 아이디를 사용
select count(m) from Member m //엔티티를 직접 사용
[SQL](JPQL 둘 다 다음 SQL 실행)
select count(m.id) as cnt from Member m
엔티티를 파라미터로 전달
String jpql = “select m from Member m where m = :member”;
List resultList = em.createQuery(jpql)
.setParameter("member", member)
.getResultList();
식별자를 직접 전달
String jpql = “select m from Member m where m.id = :memberId”;
List resultList = em.createQuery(jpql)
.setParameter("memberId", memberId)
.getResultList();
실행된 SQL
select m.* from Member m where m.id=?
엔티티 직접 사용 - 외래 키 값
Team team = em.find(Team.class, 1L);
String qlString = “select m from Member m where m.team = :team”;
List resultList = em.createQuery(qlString)
.setParameter("team", team)
.getResultList();
String qlString = “select m from Member m where m.team.id = :teamId”;
List resultList = em.createQuery(qlString)
.setParameter("teamId", teamId)
.getResultList();
m.team, m.team.id는 member에서 관리하고 있는 team의 pk 즉 tema_id 외래키 값을 의미한다.
실행된 SQL
select m.* from Member m where m.team_id=?
Named 쿼리
엔티티 같은 곳에 미리 선언을 해서 쿼리에 이름을 부여할 수 있다.
- 미리 정의해서 이름을 부여해 두고 사용하는 JPQL
- 동적 쿼리는 안되고 정적 쿼리만 가능하다.
- 어노테이션, XML을 정의할 수 있다.
- 애플리케이션 로딩 시점에 초기화 후 재사용
- 정적 쿼리는 변하지 않는다.
- 애플리케이션 로딩 시점에 JPA가 이 정적 쿼리를 SQL로 파싱 해서 캐시하고 있다.
- JPQL은 SQL로 파싱 돼서 실행돼야 되기 때문에 거기서 오는 cost가 발생한다.
- 근데 Named 쿼리 사용 시 로딩 시점에 딱 한번 파싱 해서 캐시하고 있기 때문에 cost가 거의 발생하지 않는다.
- 애플리케이션 로딩 시점에 쿼리를 검증
- 정적 쿼리에 대해서 애플리케이션 로딩 시점에 JPA가 파싱을 할 수 있다.
- 파싱을 하다가 문법이 잘못되면 컴파일 오류가 발생한다.
- 이러한 컴파일 오류가 발생해서 미리 오류를 잡아주는 게 정말 좋은 부분이다.
코드예시
@Entity
@NamedQuery(
name = "Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
...
}
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
Named 쿼리 환경에 따른 설정
- XML이 항상 우선권을 가진다.
- 애플리케이션 운영 환경에 따라 다른 XML을 배포할 수 있다.
Naemd 쿼리는 좋은 방식인데 위에처럼 엔티티에 선언해서 사용하는 방식은 지저분하다.
그래서 실무에서는 스프링 데이터 JPA에서 Query()를 사용한다.
Query()는 ()안에 JPQL을 선언할 수 있는데 선언한 쿼리가 Naemd 쿼리로 동작한다.
벌크 연산
SQL의 update문이나 delete문이다.
아래 예제를 살펴보자.
- 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
- JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행
- 재고가 10개 미만인 상품을 리스트로 조회한다.
- 상품 엔티티의 가격을 10% 증가한다.
- 트랜잭션 커밋 시점에 변경감지가 동작한다.
재고가 10개 미만인 상품을 리스트로 조회해서 나온 게 100만 건이다.
100만 건을 다 조회한 다음에 애플리케이션 100만건 루프를 돌리면서 상품 엔티티의 가격을 10% 증가한다.
그러면 트랜잭션 커밋 시점에 변경감지가 동작한다. 그래서 변경된 데이터가 100건이라면 100번의 UPDATE SQL 실행된다. 그래서 JPA에서 위와 같은 문제를 해결하기 위해 쿼리 한 번으로 다수의 상품을 업데이트하는 벌크 연산을 제공한다.
벌크 연산 예제
- 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)한다.
- executeUpdate()의 결과는 영향받은 엔티티 수 반환
- UPDATE, DELETE 지원
- INSERT(insert into .. select, )를 하이버네이트에서는 지원한다.
String qlString = "update Product p " +
"set p.price = p.price * 1.1 " +
"where p.stockAmount < :stockAmount";
int resultCount = em.createQuery(qlString)
.setParameter("stockAmount", 10)
.executeUpdate();
벌크 연산 주의
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리 한다.
그래서 아래와 같은 해결책이 존재한다.
- 영속성 컨텍스트에 아무 작업을 하지 않고 벌크 연산을 먼저 실행한다.
- 영속성 컨텍스트에 값이 이미 있으면 벌크 연산 수행 후 영속성 컨텍스트 초기화한다.
예시 코드
Member member1 = new Member();
member1.setUsername("member1");
member1.addTeam(team1);
member1.setAge(0);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member2");
member2.addTeam(team1);
member2.setAge(0);
em.persist(member2);
Member member3 = new Member();
member3.setUsername("member3");
member3.addTeam(team2);
member3.setAge(0);
em.persist(member3);
// 쿼리는 전송 시 자동으로 플러시 발생
int resultCount = em.createQuery("update Member m set m.age = 20")
.executeUpdate();
System.out.println("resultCount = " + resultCount);
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember.getAge() = " + findMember.getAge());
실행결과
Hibernate:
/* insert for
hellojpa.jpql.Team */insert
into
Team (name, id)
values
(?, ?)
Hibernate:
/* insert for
hellojpa.jpql.Team */insert
into
Team (name, id)
values
(?, ?)
Hibernate:
/* insert for
hellojpa.jpql.Member */insert
into
Member (age, memberType, TEAM_ID, username, id)
values
(?, ?, ?, ?, ?)
Hibernate:
/* insert for
hellojpa.jpql.Member */insert
into
Member (age, memberType, TEAM_ID, username, id)
values
(?, ?, ?, ?, ?)
Hibernate:
/* insert for
hellojpa.jpql.Member */insert
into
Member (age, memberType, TEAM_ID, username, id)
values
(?, ?, ?, ?, ?)
Hibernate:
/* update
Member m
set
m.age = 20 */ update Member m1_0
set
age=20
resultCount = 3
findMember.getAge() = 0
JPQL을 보내고 나서 em.clear()를 추가해 보자.
int resultCount = em.createQuery("update Member m set m.age = 20")
.executeUpdate();
System.out.println("resultCount = " + resultCount);
em.clear();
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember.getAge() = " + findMember.getAge());
실행결과
resultCount = 3
Hibernate:
select
m1_0.id,
m1_0.age,
m1_0.memberType,
m1_0.TEAM_ID,
m1_0.username
from
Member m1_0
where
m1_0.id=?
findMember.getAge() = 20
벌크 연산 실행 시 DB에는 반영이 되나 영속성 컨텍스트에 있는 값들에는 반영이 안 된다.
그래서 영속성 컨텍스트를 초기화 하지 않고 조회하면 업데이트가 안된 엔티티가 조회가 된다.
따라서 em.clear()를 하여 영속성 컨텍스트를 초기화 하고나서 em.find()를 하면 DB에 있는 값을 조회하므로 영속성 컨텍스트에 이미 값이 존재하는데 벌크 연산을 수행하면 영속성 컨텍스트를 초기화해야 한다.
Spring Data JPA에는 @Modifying() 어노테이션이 존재한다.
이걸 사용하면 벌크 연산을 사용할 수 있다.
@Modifying()을 사용하면 영속성 컨텍스트를 자동으로 초기화 해준다.
위에서 설명한 벌크 연산 수행 후 영속성 컨텍스트 초기화한다 이 부분을 쉽게 사용할 수 있게 해주는 기능이라고 보면 된다.
@Modifying
@Query("delete User u where u.active = false")
int deleteDeactivatedUsers();
참고
김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본 편 강의를 보고 정리한 내용입니다.
'JPA > JPA 기본' 카테고리의 다른 글
10. 객체지향 쿼리 언어(JPQL) - 1 (0) | 2024.08.13 |
---|---|
9. 값 타입 (0) | 2024.08.12 |
8. 프록시와 연관관계 관리 (0) | 2024.08.11 |
7. 상속관계 매핑 (0) | 2024.08.10 |
6. 다양한 연관관계 매핑 (0) | 2024.08.10 |