JPA에서 Hibernate를 구현체로 사용했을 때, getReference()의 행동을 상황별로 알아보겠습니다.
특히, getReference()을 어떻게 사용하면 불필요한 SELECT문을 줄일 수 있는지 알아보겠습니다.
사용하는 클래스는 아래와 같습니다.
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Game {
@Id
private Long id;
private String name;
}
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Player {
@Id
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private Game game;
public Player(Long id, String name) {
this.id = id;
this.name = name;
}
}
Game과 Player가 다대일로 매핑되어 있습니다.
@SpringBootTest
@Transactional
public class PlayerTest {
@PersistenceContext
EntityManager entityManager;
@BeforeEach
void setUp() {
entityManager.persist(new Game(1L, "Game 1"));
entityManager.persist(new Game(2L, "Game 2"));
entityManager.persist(new Player(1L,"Player 1"));
entityManager.persist(new Player(2L, "Player 2"));
entityManager.persist(new Player(3L, "Player 3"));
entityManager.flush();
entityManager.clear();
System.out.println("===========데이터 셋업 종료 =============");
}
}
@BeforeEach로 사용할 데이터를 미리 넣어두도록 했습니다.
@Transactional 이 테스트 코드에 붙어있으면, 기본 설정이 롤백입니다.
JPA는 DB에 변화가 없을 것으로 판단되면 쿼리문을 아예 보내지 않습니다.
다음의 테스트 코드를 실행하기 위해, flush()로 데이터를 DB에 넣어두고 clear()로 영속성 컨텍스트의 1차 캐시를 비워줍니다.
1차 캐시를 비워줘야 값을 1차 캐시에서 가져오지 않고, DB에서 가져오므로 정확한 쿼리 관찰이 가능합니다.
find()로 인스턴스 가져오기@Test
void test1(){
Game game1 = entityManager.find(Game.class, 1L);
game1.setName("Game Updated 1");
entityManager.persist(game1);
entityManager.flush();
}
=======find실행=======
Hibernate:
select
game0_.id as id1_2_0_,
game0_.name as name2_2_0_
from
game game0_
where
game0_.id=?
2023-03-02 16:19:32.785 TRACE 13540 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1]
=======find실행=======
=======flush실행=======
Hibernate:
update
game
set
name=?
where
id=?
2023-03-02 16:19:32.802 TRACE 13540 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [Game Updated 1]
2023-03-02 16:19:32.802 TRACE 13540 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [BIGINT] - [1]
=======flush실행=======
find()에서 SELECT문 실행 (1차 캐시에 저장)UPDATE문을 flush()에서 실행getReference()로 인스턴스 가져오기@DisplayName("getRef로 찾아오고 set으로 수정")
@Test
void test2(){
System.out.println("=======getReference실행=======");
Game game1 = entityManager.getReference(Game.class, 1L);
System.out.println("=======getReference실행=======");
System.out.println("=======setName실행=======");
game1.setName("Game Updated 1");
System.out.println("=======setName실행=======");
System.out.println("=======flush실행=======");
entityManager.flush();
System.out.println("=======flush실행=======");
}
=======getReference실행=======
=======getReference실행=======
=======setName실행=======
Hibernate:
select
game0_.id as id1_2_0_,
game0_.name as name2_2_0_
from
game game0_
where
game0_.id=?
2023-03-02 16:22:31.011 TRACE 29388 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1]
=======setName실행=======
=======flush실행=======
Hibernate:
update
game
set
name=?
where
id=?
2023-03-02 16:22:31.029 TRACE 29388 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [Game Updated 1]
2023-03-02 16:22:31.029 TRACE 29388 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [BIGINT] - [1]
=======flush실행=======
getReference()에서는 프록시 인스턴스만 가져옴. SELECT문 실행 안함setName()이 실행되며 SELECT문 실행UPDATE문을 flush()에서 실행Setter가 SELECT 쿼리를 발생시킨 이유?EntityManager는 엔티티 상태 정보를 가져오기 전에는 어떠한 값도 저장할 수 없습니다.
따라서 setter가 호출되었을 때, 상태 정보를 가져오기 위해 SELECT문이 나가는 것입니다.
find() 와 getReference() 을 사용했을 때, 발생되는 쿼리 갯수에 차이가 없었습니다.
setter 대신 EntityManager.remove() 를 넣어도 같은 결과가 나옵니다.
이번부터는, 관계를 연결하고 저장하는 예제를 살펴보겠습니다.
find() -> find()@Test
void test3(){
System.out.println("=======find실행=======");
Game game1 = entityManager.find(Game.class, 1L);
System.out.println("=======find실행=======");
System.out.println("=======find실행=======");
Player player1 = entityManager.find(Player.class, 1L);
System.out.println("=======find실행=======");
player1.setGame(game1);
System.out.println("=======flush실행=======");
entityManager.flush();
System.out.println("=======flush실행=======");
}
=======find실행=======
Hibernate:
select
game0_.id as id1_2_0_,
game0_.name as name2_2_0_
from
game game0_
where
game0_.id=?
2023-03-02 16:43:09.475 TRACE 35740 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1]
=======find실행=======
=======find실행=======
Hibernate:
select
player0_.id as id1_3_0_,
player0_.game_id as game_id3_3_0_,
player0_.name as name2_3_0_
from
player player0_
where
player0_.id=?
2023-03-02 16:43:09.491 TRACE 35740 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1]
=======find실행=======
=======flush실행=======
Hibernate:
update
player
set
game_id=?,
name=?
where
id=?
2023-03-02 16:43:09.497 TRACE 35740 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1]
2023-03-02 16:43:09.497 TRACE 35740 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [VARCHAR] - [Player 1]
2023-03-02 16:43:09.497 TRACE 35740 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [BIGINT] - [1]
=======flush실행=======
Game을 가져오는 find()에서 SELECT문 실행Player를 가져오는 find()에서 SELECT문 실행UPDATE문을 flush()에서 실행@DisplayName("[relation]getRef로 game, find로 player 찾아오고 setGame 정상 결과 관찰")
@Test
void test4(){
System.out.println("=======getReference실행=======");
Game game2 = entityManager.getReference(Game.class, 2L);
System.out.println("=======getReference실행=======");
System.out.println("=======find실행=======");
Player player1 = entityManager.find(Player.class, 1L);
System.out.println("=======find실행=======");
player1.setGame(game2);
System.out.println("=======flush실행=======");
entityManager.flush();
System.out.println("=======flush실행=======");
}
=======getReference실행=======
=======getReference실행=======
=======find실행=======
Hibernate:
select
player0_.id as id1_3_0_,
player0_.game_id as game_id3_3_0_,
player0_.name as name2_3_0_
from
player player0_
where
player0_.id=?
2023-03-02 16:47:07.192 TRACE 5772 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1]
=======find실행=======
=======flush실행=======
Hibernate:
update
player
set
game_id=?,
name=?
where
id=?
2023-03-02 16:47:07.213 TRACE 5772 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [2]
2023-03-02 16:47:07.213 TRACE 5772 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [VARCHAR] - [Player 1]
2023-03-02 16:47:07.213 TRACE 5772 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [BIGINT] - [1]
=======flush실행=======
앞선 경우와 차이점이 발생했습니다.
getReference()에서는 프록시 인스턴스만 가져옴. SELECT문 실행 안함Player를 가져오는 find()에서 SELECT문 실행UPDATE문을 flush()에서 실행Game의 Id가 테이블에 존재하는 것이 확실하다면, 위 방법을 사용할 수 있고, 불필요한 SELECT문 한개를 줄일 수 있습니다.
관계를 맺는 데에 필요한 정보는 프록시 인스턴스만으로 충분하기 때문입니다. (프록시 인스턴스가 Id를 가지고 있음)
Id가 존재하지 않는다면, Player의 FK인 Game Id를 설정하면서 예외가 발생합니다.
다음 예제에서 확인해보겠습니다.
@Test
void test5() {
System.out.println("=======getReference실행=======");
Game game2 = entityManager.getReference(Game.class, 3L);
System.out.println("=======getReference실행=======");
System.out.println("=======find실행=======");
Player player1 = entityManager.find(Player.class, 1L);
System.out.println("=======find실행=======");
player1.setGame(game2);
System.out.println("=======flush실행=======");
entityManager.flush();
System.out.println("=======flush실행=======");
}
=======getReference실행=======
=======getReference실행=======
=======find실행=======
Hibernate:
select
player0_.id as id1_3_0_,
player0_.game_id as game_id3_3_0_,
player0_.name as name2_3_0_
from
player player0_
where
player0_.id=?
2023-03-02 17:39:30.976 TRACE 30608 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1]
=======find실행=======
=======flush실행=======
Hibernate:
update
player
set
game_id=?,
name=?
where
id=?
2023-03-02 17:39:30.994 TRACE 30608 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [3]
2023-03-02 17:39:30.995 TRACE 30608 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [VARCHAR] - [Player 1]
2023-03-02 17:39:30.995 TRACE 30608 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [BIGINT] - [1]
2023-03-02 17:39:30.997 WARN 30608 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 23506, SQLState: 23506
예외 로그
javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement
...
Caused by: org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Referential integrity constraint violation: "FK8095BT0VV5CAPCCV9870LN2N: PUBLIC.PLAYER FOREIGN KEY(GAME_ID) REFERENCES PUBLIC.GAME(ID) (CAST(3 AS BIGINT))"; SQL statement:
update player set game_id=?, name=? where id=? [23506-214]
...
getReference()에서는 프록시 인스턴스만 가져옴. SELECT문 실행 안함Player를 가져오는 find()에서 SELECT문 실행UPDATE문을 flush()에서 실행Game 테이블에 사용하려는 Id가 존재하는지 확인하는 과정이 없으므로, Player를 UPDATE할 때, 예외가 발생하는 것을 확인할 수 있습니다.
댓글을 깜박했어요🥺