[JPA] Cascade 옵션 종류 및 예제
Cascade란?
Cascde는 영속성 전이라고 합니다. Cascade 옵션을 정의한 엔티티가 영속화되면 연관된 엔티티도 영속화되고 삭제되면 삭제되는 것 속성입니다.
@ManyToOne, @OneToMany 등 연관관계를 설정하는 부분에 옵션으로 정의되어 있습니다.
주의 : 엔티티를 영속화할 때 연관된 엔티티도 같이 영속화한다는 편리함만 제공할 뿐 연관관계를 설정하는 어노테이션과는 관련이 없습니다.
Cascade 종류
- ALL
- PERSIST
- REMOVE
- MERGE
- REFRESH
- DETACH
Cascade옵션 중 ALL, PERSIST, REMOVE, MERGE, DETACH에 대해서만 예제를 다루고 REFRESH에 대해서는 개념만 알아보겠습니다.
예제 공통 코드
// Image 엔티티
@Entity
class Image(
@Id
@GeneratedValue
var id: Long,
var title: String,
var address: String,
)
=============================
// Board 엔티티
@Entity
class Board(
@Id
@GeneratedValue
var id: Long,
var title: String,
var content: String,
@OneToMany
@JoinColumn(name = "board_id")
var images: MutableList<Image>,
)
CascadeType 예제
CascadeType.ALL - PERSIST
@Entity
class Board(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0,
var title: String,
var content: String,
@OneToMany(cascade = [CascadeType.ALL])
@JoinColumn(name = "board_id")
var images: MutableList<Image>,
)
@Service
class BoardService(
private val em: EntityManager,
) {
@Transactional
fun save() {
val images: MutableList<Image> = mutableListOf(
Image(title = "이미지1", address = "http1"),
Image(title = "이미지2", address = "http2"),
)
val board: Board = Board(title = "제목", content = "내용", images = images)
em.persist(board)
return em.find(Board::class.java,1L)
}
}
@SpringBootTest
class BoardServiceTest(
private val boardService: BoardService,
) : FreeSpec({
"CascadeType.ALL" - {
"save" - {
val save = boardService.save()
}
}
})
발생하는 쿼리
필자는 Image를 저장한 적이 없습니다. 그런데 쿼리를 보면 image에 대한 INSERT쿼리가 발생하고 있습니다.
이유는 Cascade 옵션을 ALL로 하면 Cascade에 있는 PERSIST, REMOVE, REFRESH, MERGE, DETACH가 적용이 됩니다.
즉 위 예제는 PERSIST 속성이 적용된것을 확인할 수 있습니다.
CascadeType.ALL - REMOVE
@Service
class BoardService(
private val em: EntityManager,
) {
@Transactional
fun delete() {
val images: MutableList<Image> = mutableListOf(
Image(title = "이미지1", address = "http1"),
Image(title = "이미지2", address = "http2"),
)
val board: Board = Board(title = "제목", content = "내용", images = images)
em.persist(board)
em.flush()
em.remove(board)
}
}
@SpringBootTest
class BoardServiceTest(
private val boardService: BoardService,
) : FreeSpec({
"CascadeType.ALL" - {
"delete" - {
val save = boardService.delete()
}
}
})
발생하는 쿼리
CascadeType.ALL - DETACH
@Service
class BoardService(
private val em: EntityManager,
) {
@Transactional
fun detach() {
val images: MutableList<Image> = mutableListOf(
Image(title = "이미지1", address = "http1"),
Image(title = "이미지2", address = "http2"),
)
val board: Board = Board(title = "제목", content = "내용", images = images)
em.persist(board)
val image1 = em.find(Image::class.java, 1L)
val image2 = em.find(Image::class.java, 2L)
em.flush()
em.detach(board)
println("------------detach 후 쿼리--------------")
val find = em.find(Board::class.java, 1L)
em.find(Image::class.java, 1L)
em.find(Image::class.java, 2L)
}
@Transactional
fun notDetach() {
val images: MutableList<Image> = mutableListOf(
Image(title = "이미지1", address = "http1"),
Image(title = "이미지2", address = "http2"),
)
val board: Board = Board(title = "제목", content = "내용", images = images)
em.persist(board)
em.find(Image::class.java, 1L)
em.find(Image::class.java, 2L)
em.flush()
println("------------detach 안했을때 쿼리--------------")
val find = em.find(Board::class.java, 1L)
em.find(Image::class.java, 1L)
em.find(Image::class.java, 2L)
}
}
@SpringBootTest
class BoardServiceTest(
private val boardService: BoardService,
) : FreeSpec({
"CascadeType.ALL" - {
"detach" - {
teamApiService.detach()
}
"notDetach" - {
teamApiService.notDetach()
}
}
})
발생하는 쿼리 - DETACH
em.persist를 하여 1차 캐시에 Board와 Image에 대한 정보가 담겨 있는데 영속성 컨텍스트를 em.detach를 하여 엔티티 생명주기를 준영속 상태가 되었습니다. 그래서 Board와 Image 엔티티를 찾아올 때 SELECT 쿼리가 발생하는 것을 확인할 수 있습니다.
발생하는 쿼리 - NOT DETACH
DETACH를 안 했을 때 em.persist로 Board와 Image 엔티티는 1차 캐시에 영속 상태로 있어 Board와 Image 엔티티를 찾아올 때 DB를 통해 가져오는 게 아닌 1차 캐시에서 가져오기 때문에 SELECT 쿼리가 발생하지 않는 것을 확인할 수 있습니다.
CascadeType.ALL - MERGE
@Service
class BoardService(
private val em: EntityManager,
) {
@Transactional
fun merge() {
val images: MutableList<Image> = mutableListOf(
Image(title = "이미지1", address = "http1"),
Image(title = "이미지2", address = "http2"),
)
val board: Board = Board(title = "제목", content = "내용", images = images)
em.persist(board)
val image1 = em.find(Image::class.java, 1L)
val image2 = em.find(Image::class.java, 2L)
em.flush()
em.detach(board)
val mergeBoard = em.merge(board)
val mergeImage1 = em.find(Image::class.java, 1L)
val mergeImage2 = em.find(Image::class.java, 2L)
println("board == mergeBoard => ${board == mergeBoard}")
println("image1 == mergeImage1 => ${image1 == mergeImage1}")
println("image2 == mergeImage2 => ${image2 == mergeImage2}")
}
}
@SpringBootTest
class BoardServiceTest(
private val boardService: BoardService,
) : FreeSpec({
"CascadeType.ALL" - {
"merge" - {
teamApiService.merge()
}
}
})
결과
결과는 false가 나옵니다. 이유는 준영속 상태에서 merge를 통해 영속상태가 되었다고 하더라도 준영속되기 전 영속상태의 객체와는 전혀 다른 객체이기 때문입니다.
REFRESH
REFRESH옵션은 데이터베이스로부터 데이터를 다시 읽어 들이는 refresh 동작을 부모에서 자식 엔티티로 전파하는 옵션입니다.
Cascade 설정을 안 하고 Board를 저장을 하게 되면 어떻게 될까?
결과는 아래와 같은 오류가 발생합니다.
org.hibernate.TransientObjectException: object references an unsaved transient instance -
save the transient instance before flushing: team.guin.domain.example.Image
java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance -
save the transient instance before flushing: team.guin.domain.example.Image
이유
Board에 대해 저장이 이루어질 때 Image는 Board와 달리 비영속 상태이기 때문에 발생하는 오류입니다. 만약 Board가 저장될 때 Image가 같이 저장되길 원하고 위와 같은 오류를 안 만나기 위해서는 CascadeType.PERSIST를 사용하거나 @Transient를 사용합니다.
@Transient 사용
@Entity
class Board(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0,
var title: String,
var content: String,
@OneToMany
@JoinColumn(name = "board_id")
@Transient
var images: MutableList<Image>,
)
@Transient를 사용했을 때 발생하는 쿼리
Board만 저장되는 insert쿼리를 확인할 수 있습니다. 이유는 @Transient를 사용하면 Image를 영속화를 하지 않은 상태에서 Board를 저장하는 것이기 때문입니다. 그런데 필자는 Board가 저장될 때 Image도 같이 저장되는 것을 원합니다. 그러기 위해선 위에서 말한 설명한 Cascade옵션을 설정해 주면 가능합니다.
정리
Cascade는 영속성 전이입니다. Cascade 옵션을 사용하는 엔티티가 영속화가 되면 연관된 엔티티에게 영속성 전이가 됩니다.
위 예제를 보면 CascadeType.ALL로 설정을 해놓고 Board에 대해서만 em.persist, em.detach, em.merge, em.remove 행위를 하였습니다. 그런데 결과를 보면 연관되어 있는 Image엔티티에도 Board와 동일하게 적용된 것을 확인할 수 있었습니다.
만약 CascadeType.ALL이 아닌 PERSIST, REMOVE, DETACH, MERGE, REFRESH로 설정하게 된다면 설정된 옵션만 적용됩니다.
@OneToMany(cascade = [CascadeType.PERSIST, CascadeType.REMOVE])
- ALL : PERSIST, REMOVE, MERGE, DETACH, REFRESH 모두 적용됩니다.
- PERSIST : 연관된 엔티티를 영속 상태로 만들어줍니다.
- REMOVE : 연관된 엔티티도 함께 삭제됩니다.
- DETACH : 연관된 엔티티도 함께 준영속상태로 만들어줍니다.
- MERGE : 연관된 엔티티도 함께 새로운 영속 상태로 만들어줍니다.
- REFRESH : 데이터베이스로부터 데이터를 다시 읽어 들이는 refresh 동작을 연관된 엔티티로 전파합니다.
'JPA' 카테고리의 다른 글
[JPA] 2차 캐시란? (0) | 2024.01.16 |
---|---|
[JPA] JdbcTemplate과 JPA 데이터 Insert 속도 비교 (0) | 2023.06.09 |
[JPA] Kotlin을 이용하여 Soft Delete 구현 (0) | 2023.03.04 |
[JPA] N+1 원인 및 해결방법 (0) | 2023.01.18 |
[Querydsl] 스프링 부트에서 Querydsl 설정 (0) | 2022.06.14 |
댓글
이 글 공유하기
다른 글
-
[JPA] 2차 캐시란?
[JPA] 2차 캐시란?
2024.01.16 -
[JPA] JdbcTemplate과 JPA 데이터 Insert 속도 비교
[JPA] JdbcTemplate과 JPA 데이터 Insert 속도 비교
2023.06.09 -
[JPA] Kotlin을 이용하여 Soft Delete 구현
[JPA] Kotlin을 이용하여 Soft Delete 구현
2023.03.04 -
[JPA] N+1 원인 및 해결방법
[JPA] N+1 원인 및 해결방법
2023.01.18