DevLog ๐ถ
[JPA] CascadeType.REMOVE vs orphanRemoval=true ์ฐจ์ด์ ์์๋ณด๊ธฐ - 1ํธ ๋ณธ๋ฌธ
[JPA] CascadeType.REMOVE vs orphanRemoval=true ์ฐจ์ด์ ์์๋ณด๊ธฐ - 1ํธ
dolmeng2 2023. 6. 29. 23:58๐ฑ ๋ค์ด๊ฐ๊ธฐ ์
JPA๋ฅผ ๊ณต๋ถํ๋ฉด์ CascadeType.REMOVE์ orphanRemoval = true ์ต์ ์ ๋ํด์ ์ด๋ค ์ฐจ์ด๊ฐ ์๋์ง ์ ๋๋ก ์ธ์งํ ์ ์ด ์๋ ๊ฒ ๊ฐ์์, ์ด๋ฒ์ ๊ณต๋ถํ ๊ฒธ ์ฌ๋ฌ ๊ฐ์ง ํ ์คํธ๋ฅผ ์งํํด๋ณด๋ฉฐ ๋ ์ต์ ์ ์ฐจ์ด๋ฅผ ๊ณต๋ถํด๋ณด์๋ค.
๊ฒฐ๊ณผ์ ์ผ๋ก ๋งํ์๋ฉด Cascade ์ต์ ์ ๋ถ๋ชจ ์ํฐํฐ์ ์์ ์ํฐํฐ์ ์์ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ๊ฒ์ด๊ณ , orphanRemoval์ ์กฐ๊ธ ๋ ์ธ๋ถ์ ์ผ๋ก ๊ณ ์ ๊ฐ์ฒด์ ๋ํ ๊ด๋ฆฌ๋ผ๋ ์๊ฐ์ด ๋ค์๋ค. ์ง๊ธ๋ถํฐ ์ฌ๋ฌ ์ผ์คํธ๋ฅผ ํ ์คํธํ๋ฉฐ ํ๋์ฉ ์์๋ณด์.
๐ฑ ์ํฐํฐ ์ธํ ํ๊ธฐ
1:N ๊ด๊ณ๋ฅผ ๋ด๋นํด์ค '์ฝ์ํธ' ์ํฐํฐ์ '์ฝ์ํธ ํฐ์ผ' ์ํฐํฐ๋ฅผ ์์ฑํ์๋ค.
๋ ์ฌ์ด์ ๊ด๊ณ๋ ์๋ฐฉํฅ์ผ๋ก ์ค์ ํ์์ผ๋ฉฐ, ํ๋์ ์ฝ์ํธ๋ ์ฌ๋ฌ ๊ฐ์ ์ฝ์ํธ ํฐ์ผ์ ๋ณด์ ํ ์ ์๋๋ก ์ค๊ณํ์๋ค.
<Concert>
@Entity
class Concert(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L,
@Column(nullable = false)
val name: String,
@Column(nullable = false)
val ticketLimit: Int,
concertTickets: MutableList<ConcertTicket> = Collections.emptyList()
) {
@OneToMany(fetch = FetchType.LAZY, mappedBy = "concert")
val concertTickets: MutableList<ConcertTicket> = concertTickets.toMutableList()
fun addTicket(concertTicket: ConcertTicket) {
concertTickets.add(concertTicket)
}
}
<ConcertTicket>
@Entity
class ConcertTicket(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L,
@Column(nullable = false)
val userId: Long,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(
name = "concert_id",
foreignKey = ForeignKey(name = "fk_ticket_concert_id")
)
var concert: Concert
)
์ฝ์ํธ ์ํฐํฐ๋ ์ฝ์ํธ ํฐ์ผ ์ํฐํฐ๋ฅผ ์ถ๊ฐํ ์ ์์ผ๋ฉฐ, ํ์ต์ ์ํด ์ธ๋ํค ์ ์ฝ ์กฐ๊ฑด์ ์ค์ ํ์๋ค.
๐ฑ ํ ์คํธ ํ๊ฒฝ ์ธํ ํ๊ธฐ
<application-test.yml>
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:testdb;MODE=MySQL
jpa:
hibernate:
ddl-auto: create-drop
properties:
hibernate:
globally_quoted_identifiers: true
format_sql: true
show-sql: true
ํ ์คํธ ํ๊ฒฝ ์ธํ ์ ์ํด application-test.yml ํ์ผ์ ์์ฑํ์๋ค.
์ด๋ show-sql ์ต์ ์ ํตํด์ JPA์์ ์ด๋ค ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋์ง ํ์ธํ์.
@DataJpaTest
@ActiveProfiles("test")
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class ConcertTest(
private val concertRepository: ConcertRepository,
private val concertTicketRepository: ConcertTicketRepository
) {
}
ํ ์คํธ์์ ์ฌ์ฉํ ๋ผ๋ ์ฝ๋์ด๋ค.
@DataJpaTest๋ฅผ ํตํด์ JPA ํ๊ฒฝ์ ํ ์คํธํ ์ ์๋๋ก ๋ง๋ค์์ผ๋ฉฐ, ํ์ฑ ํ๋กํ์ผ์ test๋ก ์ฃผ์ด์ application-test.yml์ ์ ๋ณด๋ฅผ ์ฝ์ ์ ์๋๋ก ๋ง๋ค์๋ค. ๋ํ, ์์ฑ์์์ ๋ฐ๋ก ์ฃผ์ ๋ฐ์ ์ ์๋๋ก autowireMode๋ฅผ TestConstructor.AutowireMode.ALL๋ก ์ค์ ํ์๋ค.
๐ฑ @OneToMany์์ CascadeType.REMOVE
@Entity
class Concert(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L,
@Column(nullable = false)
val name: String,
@Column(nullable = false)
val ticketLimit: Int,
concertTickets: MutableList<ConcertTicket> = Collections.emptyList()
) {
@OneToMany(
fetch = FetchType.LAZY,
mappedBy = "concert",
cascade = [CascadeType.REMOVE, CascadeType.PERSIST] // Here!
)
val concertTickets: MutableList<ConcertTicket> = concertTickets.toMutableList()
ํ ์คํธ๋ฅผ ์ํด์ ์ฝ์ํธ ํฐ์ผ ์ํฐํฐ์ ๋ํด์ CascadeType.REMOVE๋ฅผ ์ค์ ํด์ฃผ์๋ค.
์ด๋, ๋ถ๋ชจ ์ํฐํฐ ์ ์ฅ ์ ์์ ์ํฐํฐ๋ ํจ๊ป ์ ์ฅ๋๋๋ก ๋ง๋ค๊ธฐ ์ํด์ CascadeType.PERSIST๋ก ์ค์ ํด์ฃผ์๋ค.
๐ฌ ๋ถ๋ชจ ์ํฐํฐ ์ ๊ฑฐํ๊ธฐ - ์ฝ์ํธ ์ํฐํฐ ์ ๊ฑฐ
@Test
@DisplayName("@OneToMany: CascadeType.REMOVE ํ
์คํธ - ๋ถ๋ชจ ์ํฐํฐ ์ ๊ฑฐ")
fun oneToMany_cascadeType_REMOVE_test_๋ถ๋ชจ_์ํฐํฐ_์ ๊ฑฐ() {
// given
val ์ฝ์ํธ = Concert(name = "์ธ๊ธฐ ๋ง์ ์ฝ์ํธ", ticketLimit = 10)
val ์ฝ์ํธ_ํฐ์ผ1 = ConcertTicket(userId = 1L, concert = ์ฝ์ํธ)
val ์ฝ์ํธ_ํฐ์ผ2 = ConcertTicket(userId = 2L, concert = ์ฝ์ํธ)
์ฝ์ํธ.addTicket(์ฝ์ํธ_ํฐ์ผ1)
์ฝ์ํธ.addTicket(์ฝ์ํธ_ํฐ์ผ2)
concertRepository.save(์ฝ์ํธ)
val ์ ์ฅ๋_์ฝ์ํธ๋ค = concertRepository.findAll()
val ์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค = concertTicketRepository.findAll()
assertThat(์ ์ฅ๋_์ฝ์ํธ๋ค).hasSize(1)
assertThat(์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค).hasSize(2)
// when
concertRepository.delete(์ฝ์ํธ)
// then
val ์ญ์ _์ดํ_์ ์ฅ๋_์ฝ์ํธ๋ค = concertRepository.findAll()
val ์ญ์ _์ดํ_์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค = concertTicketRepository.findAll()
assertThat(์ญ์ _์ดํ_์ ์ฅ๋_์ฝ์ํธ๋ค).hasSize(0)
assertThat(์ญ์ _์ดํ_์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค).hasSize(0)
}
ํ๋์ ์ฝ์ํธ์ ๋ํด์ 2๊ฐ์ ์ฝ์ํธ ํฐ์ผ์ ๋ฐ๊ธ๋ฐ๊ณ , ์ ์ฅ ์งํ findAll์ ํตํด ์ ์ ์ฅ๋์๋์ง ํ์ธํ๋ค.
์ดํ, ๋ถ๋ชจ ์ํฐํฐ์ธ '์ฝ์ํธ ์ํฐํฐ'๋ฅผ ์ ๊ฑฐํ ๋ค์ ์กฐํ๋ฅผ ํด๋ณด๋ฉด ์ฝ์ํธ ์ํฐํฐ์ ์ฝ์ํธ ํฐ์ผ ์ํฐํฐ ๋ชจ๋๊ฐ ์ ๊ฑฐ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
์ค์ ๋ก ๋ฐ์ํ ์ฟผ๋ฆฌ๋ฅผ ํ์ธํด๋ณด๋ฉด ์ฝ์ํธ ํฐ์ผ ์ํฐํฐ๋ฅผ ์ ๊ฑฐํ๊ธฐ ์ํ delete ์ฟผ๋ฆฌ๊ฐ 2๋ฒ, ์ฝ์ํธ ์ํฐํฐ๋ฅผ ์ ๊ฑฐํ๊ธฐ ์ํ delete ์ฟผ๋ฆฌ๊ฐ 1๋ฒ ๋ฐ์ํ์ฌ ์ด 3๋ฒ์ ์ฟผ๋ฆฌ๊ฐ ๋๊ฐ ๊ฒ์ ๋ณผ ์ ์๋ค.
๐ฌ ๋ถ๋ชจ ์ํฐํฐ์ ์์ ์ํฐํฐ ์ฐ๊ด๊ด๊ณ ๋๊ธฐ
@Test
@DisplayName("@OneToMany: CascadeType.REMOVE ํ
์คํธ - ๋ถ๋ชจ ์ํฐํฐ์์ ์์ ์ํฐํฐ ์ ๊ฑฐ")
fun oneToMany_cascadeType_REMOVE_test_๋ถ๋ชจ_์ํฐํฐ์์_์์_์ํฐํฐ_์ ๊ฑฐ() {
// given
val ์ฝ์ํธ = Concert(name = "์ธ๊ธฐ ๋ง์ ์ฝ์ํธ", ticketLimit = 10)
val ์ฝ์ํธ_ํฐ์ผ1 = ConcertTicket(userId = 1L, concert = ์ฝ์ํธ)
val ์ฝ์ํธ_ํฐ์ผ2 = ConcertTicket(userId = 2L, concert = ์ฝ์ํธ)
์ฝ์ํธ.addTicket(์ฝ์ํธ_ํฐ์ผ1)
์ฝ์ํธ.addTicket(์ฝ์ํธ_ํฐ์ผ2)
concertRepository.save(์ฝ์ํธ)
val ์ ์ฅ๋_์ฝ์ํธ๋ค = concertRepository.findAll()
val ์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค = concertTicketRepository.findAll()
assertThat(์ ์ฅ๋_์ฝ์ํธ๋ค).hasSize(1)
assertThat(์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค).hasSize(2)
// when
์ฝ์ํธ.concertTickets.remove(์ฝ์ํธ_ํฐ์ผ1)
์ฝ์ํธ.concertTickets.remove(์ฝ์ํธ_ํฐ์ผ2)
// then
val ์ญ์ _์ดํ_์ ์ฅ๋_์ฝ์ํธ๋ค = concertRepository.findAll()
val ์ญ์ _์ดํ_์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค = concertTicketRepository.findAll()
assertThat(์ญ์ _์ดํ_์ ์ฅ๋_์ฝ์ํธ๋ค).hasSize(1)
assertThat(์ญ์ _์ดํ_์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค).hasSize(2)
}
๋ถ๋ชจ ์ํฐํฐ์ธ ์ฝ์ํธ ์ํฐํฐ๋ฅผ ์กฐํํ ํ, ํด๋น ์ํฐํฐ์ ๊ด๋ จ๋ ์ฝ์ํธ ํฐ์ผ ์ํฐํฐ์ ๋ํด remove๋ฅผ ์งํํ์ฌ ๋ถ๋ชจ์ ์์๊ฐ์ ์ฐ๊ด๊ด๊ณ๋ฅผ ๋์ด์ฃผ์๋ค. ์ด๋ ์ฝ์ํธ ํฐ์ผ ์ํฐํฐ 2๊ฐ๋ '๊ณ ์ ๊ฐ์ฒด'๊ฐ ๋์๋ค๊ณ ํ๋จํ๋๋ฐ, CascadeType.REMOVE์์๋ ๊ณ ์ ๊ฐ์ฒด๊ฐ ๋ฐ์ํ๋๋ผ๋ ์ค์ DB์์ ์ ๊ฑฐ๋์ง๋ ์๋๋ค.
๐ฑ @ManyToOne์์ CascadeType.REMOVE
cascadeType์ ๊ฒฝ์ฐ @OneToMany๋ฟ๋ง ์๋๋ผ @ManyToOne์์๋ ์ง์ ํ ์ ์๋ค.
@Entity
class Concert(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L,
@Column(nullable = false)
val name: String,
@Column(nullable = false)
val ticketLimit: Int,
concertTickets: MutableList<ConcertTicket> = Collections.emptyList()
) {
@OneToMany(
fetch = FetchType.LAZY,
mappedBy = "concert",
cascade = [CascadeType.PERSIST] // here!
)
val concertTickets: MutableList<ConcertTicket> = concertTickets.toMutableList()
}
@Entity
class ConcertTicket(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L,
@Column(nullable = false)
val userId: Long,
@ManyToOne(
fetch = FetchType.LAZY,
cascade = [CascadeType.REMOVE]
)
@JoinColumn(
name = "concert_id",
foreignKey = ForeignKey(name = "fk_ticket_concert_id")
)
var concert: Concert
)
๊ธฐ์กด ์ฝ์ํธ ์ํฐํฐ์ ์ ์ฉํ์๋ CascadeType.REMOVE๋ฅผ ์ ๊ฑฐํ๊ณ , ์ฝ์ํธ ํฐ์ผ ์ํฐํฐ์ ๋ํด์ ์ ์ฉํ์๋ค.
๐ฌ ์์ ์ํฐํฐ ์ ๊ฑฐํ๊ธฐ - ์ฝ์ํธ ํฐ์ผ ์ํฐํฐ ์ ๊ฑฐ
@Test
@DisplayName("@ManyToOne: CascadeType.REMOVE ํ
์คํธ - ์์ ์ํฐํฐ ์ ๊ฑฐ")
fun manyToOne_cascadeType_REMOVE_test_์์_์ํฐํฐ_์ ๊ฑฐ() {
// given
val ์ฝ์ํธ = Concert(name = "์ธ๊ธฐ ๋ง์ ์ฝ์ํธ", ticketLimit = 10)
val ์ฝ์ํธ_ํฐ์ผ1 = ConcertTicket(userId = 1L, concert = ์ฝ์ํธ)
์ฝ์ํธ.addTicket(์ฝ์ํธ_ํฐ์ผ1)
concertRepository.save(์ฝ์ํธ)
val ์ ์ฅ๋_์ฝ์ํธ๋ค = concertRepository.findAll()
val ์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค = concertTicketRepository.findAll()
assertThat(์ ์ฅ๋_์ฝ์ํธ๋ค).hasSize(1)
assertThat(์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค).hasSize(1)
// when
concertTicketRepository.delete(์ฝ์ํธ_ํฐ์ผ1);
// then
val ์ญ์ _์ดํ_์ ์ฅ๋_์ฝ์ํธ๋ค = concertRepository.findAll()
val ์ญ์ _์ดํ_์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค = concertTicketRepository.findAll()
assertThat(์ญ์ _์ดํ_์ ์ฅ๋_์ฝ์ํธ๋ค).hasSize(0)
assertThat(์ญ์ _์ดํ_์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค).hasSize(0)
}
์ ํ ์คํธ์ ๊ฒฝ์ฐ, ์ฝ์ํธ ์ํฐํฐ์ ํ ๊ฐ์ ์ฝ์ํธ ํฐ์ผ ์ํฐํฐ๊ฐ ์๋ ๊ฒฝ์ฐ์ด๋ค. (์์์ด 1๊ฐ๋ง ์๋ ๊ฒฝ์ฐ)
์ด๋ ์์ ์ํฐํฐ๋ฅผ ์ ๊ฑฐํ๊ฒ ๋๋ค๋ฉด ํด๋น ์์ ์ํฐํฐ์ ์ฐ๊ด๋ ๋ถ๋ชจ ์ํฐํฐ์ ์์ ์ํฐํฐ ๋ชจ๋๊ฐ ์ ๊ฑฐ๋๋ค.
- ์ฐธ๊ณ ๋ก, ์ฌ๊ธฐ์ ์ฐ๊ด์ด ๋์ด ์๋ค๋ ๊ฒ์ ๋จ์ํ 'DB์์ FK๋ก ์ฐ๊ฒฐ์ด ๋์ด ์๋ค'๋ผ๋ ์๋ฏธ๋ณด๋ค, ์์ํ๊ฐ ๋์ด ์๋์ง๋ฅผ ํ์ธํ๋ค. ์์ํ๋์ง ์๊ณ ๋จ์ํ FK๋ก๋ง ์ฐ๊ฒฐ์ด ๋์ด ์๋ค๋ฉด ์ฝ์ํธ ํฐ์ผ ์ํฐํฐ๋ฅผ ์ ๊ฑฐํ๋๋ผ๋ ์ฝ์ํธ ์ํฐํฐ๊น์ง ์ ๊ฑฐ๋์ง๋ ์๋๋ค.
๊ทธ๋ ๋ค๋ฉด, ์ฌ๋ฌ ๊ฐ์ ์์์ด ์์ ๋๋ ์ด๋ป๊ฒ ๋ ๊น?
@Test
@DisplayName("@ManyToOne: CascadeType.REMOVE ํ
์คํธ - ์์ ์ํฐํฐ ์ ๊ฑฐ : ์์์ด ์ฌ๋ฌ ๊ฐ์ผ ๊ฒฝ์ฐ")
fun manyToOne_cascadeType_REMOVE_test_์์_์ํฐํฐ_์ ๊ฑฐ_์์์ด_์ฌ๋ฌ๊ฐ์ผ_๊ฒฝ์ฐ() {
// given
val ์ฝ์ํธ = Concert(name = "์ธ๊ธฐ ๋ง์ ์ฝ์ํธ", ticketLimit = 10)
val ์ฝ์ํธ_ํฐ์ผ1 = ConcertTicket(userId = 1L, concert = ์ฝ์ํธ)
val ์ฝ์ํธ_ํฐ์ผ2 = ConcertTicket(userId = 2L, concert = ์ฝ์ํธ)
์ฝ์ํธ.addTicket(์ฝ์ํธ_ํฐ์ผ1)
์ฝ์ํธ.addTicket(์ฝ์ํธ_ํฐ์ผ2)
concertRepository.save(์ฝ์ํธ)
val ์ ์ฅ๋_์ฝ์ํธ๋ค = concertRepository.findAll()
val ์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค = concertTicketRepository.findAll()
assertThat(์ ์ฅ๋_์ฝ์ํธ๋ค).hasSize(1)
assertThat(์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค).hasSize(2)
// when
concertTicketRepository.delete(์ฝ์ํธ_ํฐ์ผ1)
// then
assertThatThrownBy {
concertRepository.findAll()
// concertTicketRepository.findAll()
}.isInstanceOf(DataIntegrityViolationException::class.java)
}
์ฝ์ํธ ์ํฐํฐ์ 2๊ฐ์ ์ฝ์ํธ ํฐ์ผ ์ํฐํฐ๊ฐ ์๊ณ , ์ฒซ ๋ฒ์งธ ์ฝ์ํธ ํฐ์ผ ์ํฐํฐ์ ๋ํด์ ์ ๊ฑฐํ ์ํฉ์ด๋ค.
์ด๋, ์ฝ์ํธ์ ๋ํด ์กฐํํ๋ ์์ ์์ DataIntegrityViolationException์ด ๋ฐ์ํ๋ค.
- ์ฐธ๊ณ ๋ก, ์ฃผ์์ผ๋ก concertTicketRepository.findAll()์ ํด๋์๋๋ฐ, ์ฝ์ํธ๋ ์ฝ์ํธ ํฐ์ผ ์ํฐํฐ๋ ์กฐํํ๋ ์ฟผ๋ฆฌ๋ฅผ ๋ฐ์์ํฌ ๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ๊ธฐ ๋๋ฌธ์ด๋ค.
์์ ํ ์คํธ ์ฝ๋๋ฅผ ๋๋ฆฌ๋ ๊ฐ์ฅ ์ฒ์ ์์ ์ธ ์ํฐํฐ ์ ์ฅ ๋ DB ์ํฉ์ ์๋์ ๊ฐ์ ๊ฒ์ด๋ค.
[concert]
id | name | ticket_limit |
1 | ์ธ๊ธฐ ๋ง์ ์ฝ์ํธ | 10 |
[concert_ticket]
id | user_id | concert_id |
1 | 1 | 1 |
2 | 2 | 1 |
PK = 1์ธ ์ฝ์ํธ ๋ ์ฝ๋๊ฐ ์ ์ฅ๋์ด ์๊ณ , ํด๋น ๋ ์ฝ๋๋ฅผ FK๋ก ๊ฐ์ง๊ณ ์๋ PK = 1, 2์ธ ์ฝ์ํธ ํฐ์ผ ๋ ์ฝ๋๊ฐ ์ ์ฅ๋์ด ์๋ ํํ์ด๋ค.
ํ์ง๋ง, ์ค์ ์์ฑ๋ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋ฉด 1๊ฐ์ ์ฝ์ํธ ํฐ์ผ์ ๋ํด ์ ๊ฑฐํ๊ณ , ๋ฐ๋ก ์ฝ์ํธ๋ฅผ ์ ๊ฑฐํ๋ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ค.
concert_ticket ํ ์ด๋ธ์ PK=1์ธ ์ฒซ ๋ฒ์งธ ๋ ์ฝ๋๊ฐ ์ ๊ฑฐ๋๊ณ , concert ํ ์ด๋ธ์ PK=1์ธ ์ฒซ ๋ฒ์งธ ๋ ์ฝ๋๊ฐ ์ ๊ฑฐ๋๋ ์ํฉ์ด๋ค. ํ์ง๋ง, concert ํ ์ด๋ธ์ ๋ ์ฝ๋๊ฐ ์ ๊ฑฐ๋๋ ค๊ณ ํ ๋ ์ํฐํฐ์ ๊ฑธ์ด๋์๋ ์ธ๋ํค ์ ์ฝ ์กฐ๊ฑด์ ์ํด์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๊ฒ ๋๋ค.
@Entity
class ConcertTicket(
....
@JoinColumn(
name = "concert_id",
foreignKey = ForeignKey(name = "fk_ticket_concert_id") // here
)
var concert: Concert
)
๋๋ฌธ์ concert ํ ์ด๋ธ์ PK=1์ธ ๋ ์ฝ๋๋ฅผ ์ ๊ฑฐํ๋ฉด, concert_ticket ํ ์ด๋ธ์ ๋ ๋ฒ์งธ ๋ ์ฝ๋๊ฐ ์ฐธ์กฐํ๊ณ ์๊ธฐ ๋๋ฌธ์ fk ๊ฐ์ด null์ด ๋ ์ ์์ด ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ด๋ค.
โญ๏ธ ์ deleteํ๋ ์์ ์ด ์๋, findAll()์ ํตํด ์กฐํํ๋ ์์ ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ ๊ฑธ๊น?
@Test
@DisplayName("@ManyToOne: CascadeType.REMOVE ํ
์คํธ - ์์ ์ํฐํฐ ์ ๊ฑฐ : ์์์ด ์ฌ๋ฌ ๊ฐ์ผ ๊ฒฝ์ฐ")
fun manyToOne_cascadeType_REMOVE_test_์์_์ํฐํฐ_์ ๊ฑฐ_์์์ด_์ฌ๋ฌ๊ฐ์ผ_๊ฒฝ์ฐ() {
// given
val ์ฝ์ํธ = Concert(name = "์ธ๊ธฐ ๋ง์ ์ฝ์ํธ", ticketLimit = 10)
val ์ฝ์ํธ_ํฐ์ผ1 = ConcertTicket(userId = 1L, concert = ์ฝ์ํธ)
val ์ฝ์ํธ_ํฐ์ผ2 = ConcertTicket(userId = 2L, concert = ์ฝ์ํธ)
์ฝ์ํธ.addTicket(์ฝ์ํธ_ํฐ์ผ1)
์ฝ์ํธ.addTicket(์ฝ์ํธ_ํฐ์ผ2)
concertRepository.save(์ฝ์ํธ)
val ์ ์ฅ๋_์ฝ์ํธ๋ค = concertRepository.findAll()
val ์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค = concertTicketRepository.findAll()
assertThat(์ ์ฅ๋_์ฝ์ํธ๋ค).hasSize(1)
assertThat(์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค).hasSize(2)
// when
assertDoesNotThrow {
concertTicketRepository.delete(์ฝ์ํธ_ํฐ์ผ1)
}
}
์ค์ ๋ก, ๋จ์ํ ์ฝ์ํธ ํฐ์ผ ๋ ํ์งํ ๋ฆฌ์์ delete๋ฅผ ํ๋ค๊ณ ํด์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ ์๋๋ค.
์ ์ด์ ์กฐํํ๊ธฐ ์ ๊น์ง delete ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ์ง๋ ์๋๋ค. (์์ select๋ ์ฝ์ํธ, ์ฝ์ํธ ํฐ์ผ์ ๋ํ findAll())
JPA์ ์์์ฑ ์ปจํ ์คํธ๋ฅผ ์๊ฐํ๋ฉด ๊ฐ๋จํ๊ฒ ๋ต์ด ๋์ฌ ๊ฒ์ด๋ค.
delete์ ๊ฒฝ์ฐ ์ค์ DB๋ก ์ ๊ฑฐ๋ฅผ ์ํ ์ฟผ๋ฆฌ๋ฅผ ๋ฐ์์ํค๋ ๊ฒ์ด ์๋, ๋จ์ํ '์์์ฑ ์ปจํ ์คํธ'์์ ํด๋น ์ํฐํฐ๋ฅผ '์ ๊ฑฐ ์ฒ๋ฆฌ'๋ฅผ ํ๋ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ด๋ค.
@Override
@Transactional
@SuppressWarnings("unchecked")
public void delete(T entity) {
Assert.notNull(entity, "Entity must not be null");
if (entityInformation.isNew(entity)) {
return;
}
Class<?> type = ProxyUtils.getUserClass(entity);
T existing = (T) em.find(type, entityInformation.getId(entity));
// if the entity to be deleted doesn't exist, delete is a NOOP
if (existing == null) {
return;
}
em.remove(em.contains(entity) ? entity : em.merge(entity));
}
์ค์ ๋ก, ๊ตฌํ์ฒด๋ฅผ ์ฌ์ฉ ๋ณด๋ฉด em.remove()๋ฅผ ํตํด์ entityManager์ ์กด์ฌํ๋ ์ํฐํฐ์ ๋ํด์ ์ ๊ฑฐํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
๊ทธ๋์, ์ค์ DB์ ์ ๊ฑฐ ์ฟผ๋ฆฌ๋ฅผ ๋ฐ์์ํค๊ณ ์ถ๋ค๋ฉด ์๋์ ๊ฐ์ด ํ ์คํธ๋ฅผ ์์ ํด์ผ ํ๋ค.
fun manyToOne_cascadeType_REMOVE_test_์์_์ํฐํฐ_์ ๊ฑฐ_์์์ด_์ฌ๋ฌ๊ฐ์ผ_๊ฒฝ์ฐ() {
// given
val ์ฝ์ํธ = Concert(name = "์ธ๊ธฐ ๋ง์ ์ฝ์ํธ", ticketLimit = 10)
val ์ฝ์ํธ_ํฐ์ผ1 = ConcertTicket(userId = 1L, concert = ์ฝ์ํธ)
val ์ฝ์ํธ_ํฐ์ผ2 = ConcertTicket(userId = 2L, concert = ์ฝ์ํธ)
์ฝ์ํธ.addTicket(์ฝ์ํธ_ํฐ์ผ1)
์ฝ์ํธ.addTicket(์ฝ์ํธ_ํฐ์ผ2)
concertRepository.save(์ฝ์ํธ)
val ์ ์ฅ๋_์ฝ์ํธ๋ค = concertRepository.findAll()
val ์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค = concertTicketRepository.findAll()
assertThat(์ ์ฅ๋_์ฝ์ํธ๋ค).hasSize(1)
assertThat(์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค).hasSize(2)
// when
assertDoesNotThrow {
concertTicketRepository.delete(์ฝ์ํธ_ํฐ์ผ1)
}
assertThatThrownBy {
entityManager.flush()
}.isInstanceOf(ConstraintViolationException::class.java)
}
entityManager์ ๋ํด ๊ฐ์ ๋ก ํ๋ฌ์๋ฅผ ํด์, ์์์ฑ ์ปจํ ์คํธ์ ๋ด์ฉ์ DB์ ๋ฐ์์ํค๋ ๊ฒ์ด๋ค.
์ด๋ ๊ฒ ๋๋ฉด ์๊น์ ๋ค๋ฅด๊ฒ ๋ณ๋์ ์กฐํ ์ฟผ๋ฆฌ๋ฅผ ๋ฐ์์ํค์ง ์๋๋ผ๋ delete ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ค. ๋ค๋ง, ์ธ๋ํค ์ ์ฝ ์กฐ๊ฑด์ด ๋ฐ์ํ๊ธฐ ๋๋ฌธ์ ๋๊ฐ์ด ConstraintViolationException์ ์๊ธธ ์๋ฐ์ ์๋ค.
๐ก DataIntegrityViolationException์ ConstraintViolationException์ ๋ชจ๋ ์ธ๋ํค ์ ์ฝ ์กฐ๊ฑด ์๋ฐ์ ์ํด์ ๋ฐ์ํ๋ค. ๋ค๋ง, ConstraintViolationException์ ๊ฒฝ์ฐ ์คํ๋ง ํ๋ ์์ํฌ์์ ์ ๊ณตํ๋ ์์ธ ํด๋์ค์ด๊ณ , ConstraintViolationException๋ ํ์ด๋ฒ๋ค์ดํธ์์ ์ ๊ณตํ๋ ์์ธ ํด๋์ค์ด๋ค.
๐ฌ ๋ถ๋ชจ ์ํฐํฐ์ ์์ ์ํฐํฐ ์ฐ๊ด๊ด๊ณ ๋๊ธฐ
@Test
@DisplayName("@ManyToOne: CascadeType.REMOVE ํ
์คํธ - ๋ถ๋ชจ ์ํฐํฐ์์ ์์ ์ํฐํฐ ์ ๊ฑฐ")
fun manyToOne_cascadeType_REMOVE_test_๋ถ๋ชจ_์ํฐํฐ์์_์์_์ํฐํฐ_์ ๊ฑฐ() {
// given
val ์ฝ์ํธ = Concert(name = "์ธ๊ธฐ ๋ง์ ์ฝ์ํธ", ticketLimit = 10)
val ์ฝ์ํธ_ํฐ์ผ1 = ConcertTicket(userId = 1L, concert = ์ฝ์ํธ)
val ์ฝ์ํธ_ํฐ์ผ2 = ConcertTicket(userId = 2L, concert = ์ฝ์ํธ)
์ฝ์ํธ.addTicket(์ฝ์ํธ_ํฐ์ผ1)
์ฝ์ํธ.addTicket(์ฝ์ํธ_ํฐ์ผ2)
concertRepository.save(์ฝ์ํธ)
val ์ ์ฅ๋_์ฝ์ํธ๋ค = concertRepository.findAll()
val ์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค = concertTicketRepository.findAll()
assertThat(์ ์ฅ๋_์ฝ์ํธ๋ค).hasSize(1)
assertThat(์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค).hasSize(2)
// when
์ฝ์ํธ.concertTickets.remove(์ฝ์ํธ_ํฐ์ผ1)
์ฝ์ํธ.concertTickets.remove(์ฝ์ํธ_ํฐ์ผ2)
// then
val ์ญ์ _์ดํ_์ ์ฅ๋_์ฝ์ํธ๋ค = concertRepository.findAll()
val ์ญ์ _์ดํ_์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค = concertTicketRepository.findAll()
assertThat(์ญ์ _์ดํ_์ ์ฅ๋_์ฝ์ํธ๋ค).hasSize(1)
assertThat(์ญ์ _์ดํ_์ ์ฅ๋_์ฝ์ํธ_ํฐ์ผ๋ค).hasSize(2)
}
์ด์ ์ @OneToMany์ ๊ฒฝ์ฐ, ๋ถ๋ชจ ์ํฐํฐ์์ ์์ ์ํฐํฐ๋ฅผ ์ ๊ฑฐํ๋๋ผ๋ ์๋ฌด ์ผ๋ ๋ฐ์ํ์ง ์์์๋ค.
๋ง์ฐฌ๊ฐ์ง๋ก @ManyToOne์ ์ญ์ ๊ณ ์ ๊ฐ์ฒด๊ฐ ๋ฐ์ํ๋๋ผ๋ ์ค์ DB์ delete ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ์ง ์์์ ๊ทธ๋๋ก ์กฐํ๊ฐ ๋๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
์๊ฐ๋ณด๋ค ๊ธ์ด ๊ธธ์ด์ ธ์ orphanRemoval์ ๋ํ ๋ด์ฉ์ 2ํธ์์ ์์๋ณด๋๋ก ํ์!