flush
영속성 컨텍스트의 변경 내용을 DB에 반영하는 이벤트
JPQL
JPQL : Jpa의 Entity 객체들을 이용해서 Query할수있는 객체 지향 쿼리
예제
/**
* User.java
*/
@Entity
@Getter @Setter
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public User(String name) {
this.name = name;
}
}
실행하려는 로직은 아래와 같습니다.
- user 4개 insert
- JPQL을 이용해서 4번째 insert한 user의 name을 "changed name by jpql"로 변경
- 4번째 user의 정보를 다시 가져와서 이름이 정상적으로 "changed name by jpql"로 변경 되었는지 검증
위 로직의 테스트 코드입니다.
@Test
@Transactional
void JPQL_영속화_테스트() {
/// user1,2,3,4 생성 및 영속화
User user1 = new User("user1 name");
User user2 = new User("user2 name");
User user3 = new User("user3 name");
User user4 = new User("user4 name");
em.persist(user1);
em.persist(user2);
em.persist(user3);
em.persist(user4);
/// JPQL을 이용해서 4번째 insert된 user4의 이름을 'changed name by jpql'로 update
Query query = em.createQuery(
"UPDATE User u set u.name = 'changed name by querydsl' where u.id = 4"
);
query.executeUpdate();
/// update된 user4의 정보를 가져옴
User updatedUser4 = em.find(User.class, 4L);
/// user4의 이름이 'changed name by jpql'로 변경되었는지 검증
Assertions.assertEquals(updatedUser4.getName(), "changed name by jpql");
}
위 코드의 결과는 Fail입니다.
Why
위 사진은 테스트코드에서 실제 실행된 쿼리입니다.
4번 insert가 되고 update 쿼리도 정상적으로 나갔습니다.
하지만 마지막에 다시 find하는 쿼리는 실행되지 않은채로 name을 비교하고 결국 fail이 발생합니다.
위 결과는 JPQL과 영속성 컨텍스트의 동작방식에 대해 숙지가 되어있어야 이해가 가능합니다.
JPQL
JPQL은 영속성 컨텍스트의 데이터 변화에 관여하지 않고 실행되기 전에 flush가 자동 실행됩니다.
영속성 컨텍스트의 데이터가 아직 DB에 동기화가 되지 않은 상태에서 JPQL이 먼저 실행되버리면 DB와 영속성 컨텍스트의 일관성은 바로 무너지게 되기 때문입니다.
그러므로 JPQL은 실행전에 flush
를 실행시켜 영속성 컨텍스트와 DB의 일관성을 먼저 확보한 뒤에 실행되게 됩니다.
다시 돌아와서, 왜 마지막 find에서 SELECT 쿼리가 실행되지 않았는가?
- 4개의 user는 persist로 영속성 컨텍스트에 올라가있는 상태가 됩니다.
이 상태에서 JPQL을 이용해서 update 쿼리를 실행시키면 update쿼리는 바로 실행됩니다.
하지만 이 경우는dirty checking
(변경 감지)가 아닌 JPQL로 실행되었기 때문에 update내용이 영속성 컨텍스트에 반영되지는 않습니다.
즉, update가 쿼리가 실행되었다해도 영속성 컨텍스트에는 아직 update이전의 name이 남아있게 됩니다. - 이런 상태에서 EntityManager의 find를 실행하게 되는데 EntityManager의 find는 우선적으로 영속성 컨텍스트에서 해당 데이터가 있는지 검색한 후에 없으면 쿼리를 실행하도록 되어있습니다.
- 그런데 user들을 생성하면서 영속성 컨텍스트에는 이미 올라가있는 상태이기때문에 4번 user를 find하는 쿼리는 실행되지 않게 됩니다.
그러므로 JPQL은 실행전에 flush
를 실행시켜 영속성 컨텍스트와 DB의 일관성을 먼저 확보한 뒤에 실행되게 됩니다.결국 select쿼리를 실행하지 않고 아직 update되지 않은 영속성 컨텍스트에 남아있는 user4의 정보를 가져오게 되어 위 TEST는 fail이 발생하게 됩니다.
TEST코드가 SUCCESS되게 하려면?
단순합니다. 영속성 컨텍스트와 JPQL의 결과가 일관되지 않기 때문에 영속성 컨텍스트를 다시 구축하도록 영속성 컨텍스트를 초기화하면 됩니다.
변경된 코드
@Test
@Transactional
void JPQL_영속화_테스트() {
/// user1,2,3,4 생성 및 영속화
User user1 = new User("user1 name");
User user2 = new User("user2 name");
User user3 = new User("user3 name");
User user4 = new User("user4 name");
em.persist(user1);
em.persist(user2);
em.persist(user3);
em.persist(user4);
/// JPQL을 이용해서 4번째 insert된 user4의 이름을 'changed name by jpql'로 update
Query query = em.createQuery(
"UPDATE User u set u.name = 'changed name by querydsl' where u.id = 4"
);
query.executeUpdate();
/// JPQL 실행 이후에 영속성 컨텍스트 초기화
em.flush();
em.clear();
/// update된 user4의 정보를 가져옴
User updatedUser4 = em.find(User.class, 4L);
/// user4의 이름이 'changed name by jpql'로 변경되었는지 검증
Assertions.assertEquals(updatedUser4.getName(), "changed name by jpql");
}
실행 결과
위와 같은 경우의 해결을 위한 예제는 아닙니다.
위와 같은 상황을 해결할 수 있는 내용이 될 수도 있지만 JPQL과 영속성 컨텍스트간의 동작 방식에 대해 이해한 내용을 기록해두고자 쓴 글입니다. 파고들기에 너무 좋은예제가 생겨서 JPA에 한층 더 가까워 질 수 있었던 정리과정이였습니다.
'JPA' 카테고리의 다른 글
Proxy형태로 동작하는 JPA @Transactional (8) | 2021.02.10 |
---|---|
[JPA] OneToOne 성능 튜닝 사례 1 (1) | 2020.08.18 |
MultipleBagFetchException과 default_batch_fetch_size (0) | 2020.07.13 |
LazyInitializationException- no session(준영속상태에서의 참조) (0) | 2020.07.11 |
JPA :: 영속성 컨텍스트 (0) | 2020.07.10 |