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
할 때, 예외가 발생하는 것을 확인할 수 있습니다.