DevLog 😢

[JPA] CascadeType.REMOVE vs orphanRemoval=true 차이점 μ•Œμ•„λ³΄κΈ° - 2편 λ³Έλ¬Έ

Back-end/JPA

[JPA] CascadeType.REMOVE vs orphanRemoval=true 차이점 μ•Œμ•„λ³΄κΈ° - 2편

dolmeng2 2023. 6. 30. 11:14

🌱 λ“€μ–΄κ°€κΈ° μ „

μ§€λ‚œ ν¬μŠ€νŒ…μ—μ„œλŠ” CascadeType.REMOVE에 λŒ€ν•΄μ„œ μ€‘μ μ μœΌλ‘œ μ•Œμ•„λ΄€μ—ˆλŠ”λ°, μ΄λ²ˆμ—λŠ” orphanRemoval=true μ˜΅μ…˜μ— λŒ€ν•΄μ„œ ν•œ 번 μ•Œμ•„λ³΄μž. μ—”ν‹°ν‹° μ„ΈνŒ…μ€ μ§€λ‚œ 번과 거의 λ™μΌν•˜κΈ° λ•Œλ¬Έμ— λ³€ν™”κ°€ 생긴 뢀뢄에 λŒ€ν•΄μ„œλ§Œ λ”°λ‘œ μ§šλ„λ‘ ν•˜κ² λ‹€.

 


 

🌱 μ—”ν‹°ν‹° μˆ˜μ •ν•˜κΈ°

μ΄λ²ˆμ—λŠ” CascadeType.REMOVE λŒ€μ‹ μ— orphanRemoval=trueλ₯Ό μ μš©ν•˜μž.

@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],
        orphanRemoval = true // here!
    )
    val concertTickets: MutableList<ConcertTicket> = concertTickets.toMutableList()
}

 


 

🌱 orphanRemoval = true 

πŸ’¬ λΆ€λͺ¨ μ—”ν‹°ν‹° μ œκ±°ν•˜κΈ° - μ½˜μ„œνŠΈ μ—”ν‹°ν‹° μ œκ±°

@Test
@DisplayName("orphanRemoval=true ν…ŒμŠ€νŠΈ - λΆ€λͺ¨ μ—”ν‹°ν‹° 제거")
fun orphanRemoval_true_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개의 μ½˜μ„œνŠΈ ν‹°μΌ“ μ—”ν‹°ν‹°κ°€ μ‘΄μž¬ν•˜λŠ” ν˜•νƒœμ΄λ‹€.

μ΄λ•Œ, μ½˜μ„œνŠΈ μ—”ν‹°ν‹°λ₯Ό μ œκ±°ν•˜κ²Œ 되면 CascadeType.REMOVE와 λ§ˆμ°¬κ°€μ§€λ‘œ λΆ€λͺ¨μ™€ μ—°κ΄€λœ μžμ‹ 엔티티도 ν•¨κ»˜ μ œκ±°λ˜μ–΄ 쑰회된 μ½˜μ„œνŠΈ ν‹°μΌ“ μ—”ν‹°ν‹°μ˜ κ°œμˆ˜κ°€ 0개인 것을 확인할 수 μžˆλ‹€.

μ‹€μ œλ‘œ λ°œμƒν•œ 쿼리λ₯Ό 보더라도 μ½˜μ„œνŠΈ ν‹°μΌ“ μ—”ν‹°ν‹° 2κ°œμ— λŒ€ν•œ delete 쿼리와 μ½˜μ„œνŠΈ 엔티티에 λŒ€ν•œ 1개의 delete 쿼리가 λ°œμƒν•˜μ—¬, 총 3개의 쿼리가 λ‚˜κ°„ 것을 확인할 수 μžˆλ‹€.

 


 

πŸ’¬ λΆ€λͺ¨ μ—”티티와 μžμ‹ μ—”ν‹°ν‹° μ—°κ΄€κ΄€κ³„ λŠκΈ°

@Test
@DisplayName("orphanRemoval=true ν…ŒμŠ€νŠΈ - λΆ€λͺ¨ μ—”ν‹°ν‹°μ—μ„œ μžμ‹ μ—”ν‹°ν‹° 제거")
fun orphanRemoval_true_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) // here!
    assertThat(μ‚­μ œ_이후_μ €μž₯된_μ½˜μ„œνŠΈ_ν‹°μΌ“λ“€).hasSize(0)
}

μ΄λ²ˆμ—λŠ” κ³ μ•„ 객체λ₯Ό λ§Œλ“  결과이닀.

ν•˜μ§€λ§Œ, CascadeType.REMOVE와 λ‹€λ₯΄κ²Œ λ‹¨μˆœνžˆ μ½˜μ„œνŠΈ 엔티티와 μ½˜μ„œνŠΈ ν‹°μΌ“ μ—”ν‹°ν‹° μ‚¬μ΄μ˜ 관계λ₯Ό λŠμ–΄μ£Όλ‹ˆ μ½˜μ„œνŠΈ ν‹°μΌ“ μ—”ν‹°ν‹°κ°€ μ‹€μ œ DBμ—μ„œ 제거된 것을 확인할 수 μžˆλ‹€. 즉, λΆ€λͺ¨ 엔티티와 μžμ‹ μ—”ν‹°ν‹°μ˜ 관계가 λŠμ–΄μ§€κ²Œ 되면 κ³ μ•„(orphan)κ°€ 되고, κ³ μ•„ 객체에 λŒ€ν•΄ delete 쿼리(remove)κ°€ λ°œμƒν•˜λŠ” 것이닀.

μ‹€μ œλ‘œ λ°œμƒν•œ 쿼리λ₯Ό ν™•μΈν•˜λ©΄ μ½˜μ„œνŠΈ ν‹°μΌ“ μ—”ν‹°ν‹° 2κ°œμ— λŒ€ν•œ delete 쿼리가 λ°œμƒν•œ 것을 λ³Ό 수 μžˆλ‹€.

 


 

🌱 CascadeType.REMOVE + orphanRemoval = true ν•¨κ»˜ μ‚¬μš©ν•˜κΈ°

@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, CascadeType.REMOVE],
        orphanRemoval = true
    )
    val concertTickets: MutableList<ConcertTicket> = concertTickets.toMutableList()
}

 


 

πŸ’¬ λΆ€λͺ¨ μ—”ν‹°ν‹° μ œκ±°ν•˜κΈ° - μ½˜μ„œνŠΈ μ—”ν‹°ν‹° μ œκ±°

@Test
@DisplayName("CascadeType.REMOVE + orphanRemoval=true ν…ŒμŠ€νŠΈ - λΆ€λͺ¨ μ—”ν‹°ν‹° 제거")
fun cascadeType_remove_orphanRemoval_true_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)
}

CascadeType.REMOVEλ‚˜ orphanRemoval = true μ˜΅μ…˜ λͺ¨λ‘ λΆ€λͺ¨ μ—”ν‹°ν‹°λ₯Ό μ œκ±°ν•˜λ©΄ μ—°κ΄€λœ μžμ‹ 엔티티도 ν•¨κ»˜ 제거되기 λ•Œλ¬Έμ— λ‹Ήμ—°ν•˜κ²Œ 두 μ˜΅μ…˜μ„ ν•¨κ»˜ μ‚¬μš©ν•˜λ©΄ λ˜‘κ°™μ΄ μ œκ±°λ˜λŠ” 것을 λ³Ό 수 μžˆλ‹€. 

 


 

πŸ’¬ λΆ€λͺ¨ μ—”티티와 μžμ‹ μ—”ν‹°ν‹° μ—°κ΄€κ΄€κ³„ λŠκΈ°

@Test
@DisplayName("CascadeType.REMOVE + orphanRemoval=true - λΆ€λͺ¨ μ—”ν‹°ν‹°μ—μ„œ μžμ‹ μ—”ν‹°ν‹° 제거")
fun cascadeType_remove_orphanRemoval_true_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(0)
}

같은 이유둜 λΆ€λͺ¨ 엔티티와 μžμ‹ μ—”ν‹°ν‹°μ˜ 연관관계λ₯Ό λŠλ”λΌλ„ orphanRemoval = true μ˜΅μ…˜μ— μ˜ν•΄μ„œ μ œκ±°λ˜λŠ” 것을 확인할 수 μžˆλ‹€.

 


 

🌱 orphanRemoval = trueλŠ” μ–Έμ œ μ‚¬μš©μ„ μ§€μ–‘ν•˜λŠ” 게 μ’‹μ„κΉŒ?

@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()
}

@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
)

@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    val name: String,

    concertTickets: MutableList<ConcertTicket> = Collections.emptyList()
) {

    @OneToMany(
        fetch = FetchType.LAZY,
        orphanRemoval = true
    )
    val concertTickets: MutableList<ConcertTicket> = concertTickets.toMutableList()
}

ν…ŒμŠ€νŠΈλ₯Ό μœ„ν•΄μ„œ μ‚¬μš©μžλ₯Ό λ‚˜νƒ€λ‚΄λŠ” 'User' μ—”ν‹°ν‹°λ₯Ό λ§Œλ“€κ³ , μ‚¬μš©μž μ—”ν‹°ν‹°κ°€ μ½˜μ„œνŠΈ ν‹°μΌ“ μ—”ν‹°ν‹°λ₯Ό μžμ‹μœΌλ‘œ 가지고 μžˆλŠ” μƒνƒœλ‘œ λ§Œλ“€μ—ˆλ‹€. 즉, 'μ½˜μ„œνŠΈ ν‹°μΌ“ μ—”ν‹°ν‹°'에 λŒ€ν•΄μ„œλŠ” 'μ½˜μ„œνŠΈ'와 'μ‚¬μš©μž'λΌλŠ” 두 개의 λΆ€λͺ¨ μ—”ν‹°ν‹°κ°€ μ‘΄μž¬ν•˜λŠ” ν˜•νƒœμΈ 것이닀.

참고둜, μ½˜μ„œνŠΈ ν‹°μΌ“ 엔티티에 λŒ€ν•΄μ„œλŠ” μ‚¬μš©μž 엔티티에 λŒ€ν•œ 연관관계λ₯Ό μ„€μ •ν•˜μ§€ μ•Šμ•˜λ‹€. (단방ν–₯) λ˜ν•œ, κΈ°μ‘΄ μ½˜μ„œνŠΈ μ—”ν‹°ν‹°μ˜ CascadeType.PERSIST μ˜΅μ…˜μ„ μ œκ±°ν•˜κ³  μ‚¬μš©μž μ—”ν‹°ν‹°κ°€ 가진 μ½˜μ„œνŠΈ ν‹°μΌ“ 엔티티에 λŒ€ν•΄μ„œ orphanRemoval=trueλ₯Ό μΆ”κ°€ν•˜μ˜€λ‹€.

 

@Test
@DisplayName("orphanRemoval=true - 닀쀑 λΆ€λͺ¨λ₯Ό 가지고 μžˆλŠ” 경우, ν•˜λ‚˜μ˜ λΆ€λͺ¨ μ‚­μ œ")
fun orphanRemoval_true_multiple_parents_λΆ€λͺ¨_μ—”ν‹°ν‹°_제거() {
    // given
    val μ½˜μ„œνŠΈ = Concert(name = "인기 λ§Žμ€ μ½˜μ„œνŠΈ", ticketLimit = 10)

    val μ½˜μ„œνŠΈ_ν‹°μΌ“1 = ConcertTicket(userId = 1L, concert = μ½˜μ„œνŠΈ)
    val μ½˜μ„œνŠΈ_ν‹°μΌ“2 = ConcertTicket(userId = 2L, concert = μ½˜μ„œνŠΈ)
    concertRepository.save(μ½˜μ„œνŠΈ)
    concertTicketRepository.saveAll(listOf(μ½˜μ„œνŠΈ_ν‹°μΌ“1, μ½˜μ„œνŠΈ_ν‹°μΌ“2))

    val μ‚¬μš©μž = User(name = "μ Έλ‹ˆ")
    userRepository.save(μ‚¬μš©μž)

    // when
    userRepository.delete(μ‚¬μš©μž)

    // then
    val μ‚­μ œ_이후_μ €μž₯된_μ½˜μ„œνŠΈλ“€ = concertRepository.findAll()
    val μ‚­μ œ_이후_μ €μž₯된_μ½˜μ„œνŠΈ_ν‹°μΌ“λ“€ = concertTicketRepository.findAll()
    val λͺ¨λ“ _μ‚¬μš©μžλ“€ = userRepository.findAll()

    assertThat(μ‚­μ œ_이후_μ €μž₯된_μ½˜μ„œνŠΈλ“€).hasSize(1)
    assertThat(μ‚­μ œ_이후_μ €μž₯된_μ½˜μ„œνŠΈ_ν‹°μΌ“λ“€).hasSize(2)
    assertThat(λͺ¨λ“ _μ‚¬μš©μžλ“€).hasSize(0)
}

μš°μ„ , PERSIST 없이 μ—¬λŸ¬ λΆ€λͺ¨κ°€ μžˆμ„ λ•Œ ν•˜λ‚˜μ˜ λΆ€λͺ¨μ— λŒ€ν•΄μ„œ 제거λ₯Ό ν•˜κ²Œ 되면 λ‹¨μˆœνžˆ μ‚¬μš©μžμ— λŒ€ν•œ delete 쿼리만 λ°œμƒν•œλ‹€.

 

@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    val name: String,

    concertTickets: List<ConcertTicket> = Collections.emptyList()
) {

    @OneToMany(
        fetch = FetchType.LAZY,
        cascade = [CascadeType.PERSIST],
        orphanRemoval = true
    )
    val concertTickets: MutableList<ConcertTicket> = concertTickets.toMutableList()
}

ν•˜μ§€λ§Œ, μ‚¬μš©μžμ™€ μ½˜μ„œνŠΈ 엔티티에 λŒ€ν•΄μ„œ cascade μ˜΅μ…˜μ„ 톡해 PERSISTλ₯Ό μ€€λ‹€λ©΄ κ²°κ³Όκ°€ λ‹¬λΌμ§€κ²Œ λœλ‹€.

 


 

πŸ’¬ λΆ€λͺ¨ μ—”ν‹°ν‹° μ œκ±°ν•˜κΈ° - μ‚¬μš©μž μ—”ν‹°ν‹° 제거 (닀쀑 λΆ€λͺ¨)

@Test
@DisplayName("orphanRemoval=true - 닀쀑 λΆ€λͺ¨λ₯Ό 가지고 μžˆλŠ” 경우, ν•˜λ‚˜μ˜ λΆ€λͺ¨ μ‚­μ œ")
fun orphanRemoval_true_multiple_parents_λΆ€λͺ¨_μ—”ν‹°ν‹°_제거() {
    // given
    val μ½˜μ„œνŠΈ = Concert(name = "인기 λ§Žμ€ μ½˜μ„œνŠΈ", ticketLimit = 10)

    val μ½˜μ„œνŠΈ_ν‹°μΌ“1 = ConcertTicket(userId = 1L, concert = μ½˜μ„œνŠΈ)
    val μ½˜μ„œνŠΈ_ν‹°μΌ“2 = ConcertTicket(userId = 2L, concert = μ½˜μ„œνŠΈ)
    concertRepository.save(μ½˜μ„œνŠΈ)

    val μ‚¬μš©μž = User(name = "μ Έλ‹ˆ", concertTickets = mutableListOf(μ½˜μ„œνŠΈ_ν‹°μΌ“1, μ½˜μ„œνŠΈ_ν‹°μΌ“2))
    userRepository.save(μ‚¬μš©μž)

    // when
    userRepository.delete(μ‚¬μš©μž)

    // then
    val μ‚­μ œ_이후_μ €μž₯된_μ½˜μ„œνŠΈλ“€ = concertRepository.findAll()
    val μ‚­μ œ_이후_μ €μž₯된_μ½˜μ„œνŠΈ_ν‹°μΌ“λ“€ = concertTicketRepository.findAll()
    val λͺ¨λ“ _μ‚¬μš©μžλ“€ = userRepository.findAll()

    assertThat(μ‚­μ œ_이후_μ €μž₯된_μ½˜μ„œνŠΈλ“€).hasSize(1)
    assertThat(μ‚­μ œ_이후_μ €μž₯된_μ½˜μ„œνŠΈ_ν‹°μΌ“λ“€).hasSize(0)
    assertThat(λͺ¨λ“ _μ‚¬μš©μžλ“€).hasSize(0)
}

ν•˜μ§€λ§Œ, μ‚¬μš©μžλ₯Ό μ €μž₯ν•  λ•Œ μ½˜μ„œνŠΈ 티켓을 ν•¨κ»˜ μ €μž₯ν•˜κ³  (PERSIST에 μ˜ν•΄μ„œ μ˜μ†ν™” -> insert) μ‚¬μš©μž μ—”ν‹°ν‹°λ₯Ό μ œκ±°ν•˜κ²Œ 되면 μ˜μ†ν™”λœ μ½˜μ„œνŠΈ ν‹°μΌ“ 엔티티도 ν•¨κ»˜ μ œκ±°λœλ‹€. 즉, ⭐️ κ°€μž₯ 큰 ν¬μΈνŠΈλŠ” μ˜μ†ν™”λ₯Ό ν†΅ν•΄μ„œ λΆ€λͺ¨μ™€ μžμ‹μ˜ 생λͺ…μ£ΌκΈ°λ₯Ό λ™μΌν•˜κ²Œ λ§Œλ“œλŠ” 것이닀.

 

λ§Œμ•½ μ‚¬μš©μžμ— λŒ€ν•œ μ •λ³΄λ§Œ μ œκ±°ν•˜κ³  μ‹Άκ³  μ½˜μ„œνŠΈ 티켓에 λŒ€ν•΄μ„œλŠ” μ œκ±°ν•˜κ³  싢지 μ•Šμ€ 상황이 λ°œμƒν•œλ‹€λ©΄ (λ¬Όλ‘ , 이런 상황은 거의 없을 것이닀. λŒ€λΆ€λΆ„ μžμ‹μ˜ 생λͺ…μ£ΌκΈ°λŠ” λΆ€λͺ¨μ—κ²Œ μ’…μ†λ˜λŠ” 것이 μΌλ°˜μ μ΄λ‹ˆκΉŒ) orphanRemoval = true의 μ‚¬μš©μ„ 잘 고민해봐야 ν•œλ‹€. 


 

πŸ’¬ λΆ€λͺ¨ μ—”티티와 μžμ‹ μ—”ν‹°ν‹° μ—°κ΄€κ΄€κ³„ λŠκΈ°

@Test
@DisplayName("orphanRemoval=true - 닀쀑 λΆ€λͺ¨λ₯Ό 가지고 μžˆλŠ” 경우, ν•˜λ‚˜μ˜ λΆ€λͺ¨μ—μ„œ μžμ‹ μ—”ν‹°ν‹° 제거")
fun orphanRemoval_true_multiple_parents_λΆ€λͺ¨μ—μ„œ_μžμ‹_μ—”ν‹°ν‹°_제거() {
    // given
    val μ½˜μ„œνŠΈ = Concert(name = "인기 λ§Žμ€ μ½˜μ„œνŠΈ", ticketLimit = 10)
    concertRepository.save(μ½˜μ„œνŠΈ)

    val μ½˜μ„œνŠΈ_ν‹°μΌ“1 = ConcertTicket(userId = 1L, concert = μ½˜μ„œνŠΈ)
    val μ½˜μ„œνŠΈ_ν‹°μΌ“2 = ConcertTicket(userId = 2L, concert = μ½˜μ„œνŠΈ)
    val μ‚¬μš©μž = User(name = "μ Έλ‹ˆ", concertTickets = mutableListOf(μ½˜μ„œνŠΈ_ν‹°μΌ“1, μ½˜μ„œνŠΈ_ν‹°μΌ“2))
    userRepository.save(μ‚¬μš©μž)

    // when
    μ‚¬μš©μž.concertTickets.remove(μ½˜μ„œνŠΈ_ν‹°μΌ“1)
    μ‚¬μš©μž.concertTickets.remove(μ½˜μ„œνŠΈ_ν‹°μΌ“2)

    // then
    val μ‚­μ œ_이후_μ €μž₯된_μ½˜μ„œνŠΈλ“€ = concertRepository.findAll()
    val μ‚­μ œ_이후_μ €μž₯된_μ½˜μ„œνŠΈ_ν‹°μΌ“λ“€ = concertTicketRepository.findAll()
    val λͺ¨λ“ _μ‚¬μš©μžλ“€ = userRepository.findAll()

    assertThat(μ‚­μ œ_이후_μ €μž₯된_μ½˜μ„œνŠΈλ“€).hasSize(1)
    assertThat(μ‚­μ œ_이후_μ €μž₯된_μ½˜μ„œνŠΈ_ν‹°μΌ“λ“€).hasSize(0)
    assertThat(λͺ¨λ“ _μ‚¬μš©μžλ“€[0].concertTickets).hasSize(0)
}

μ΄λ²ˆμ—λŠ” μ˜μ†μ„± 전이 μ˜΅μ…˜μ„ μ„€μ •ν•œ λΆ€λͺ¨ μ—”ν‹°ν‹° (μ‚¬μš©μž)μ—μ„œ μžμ‹ μ—”ν‹°ν‹° (μ½˜μ„œνŠΈ ν‹°μΌ“)에 λŒ€ν•œ 연관관계λ₯Ό λŠμ–΄λ³΄μž. μ½˜μ„œνŠΈ ν‹°μΌ“ μ—”ν‹°ν‹°λŠ” DB μƒμœΌλ‘œ μ½˜μ„œνŠΈμ™€ 연관관계가 μžˆμŒμ—λ„ μ˜μ†μ„± 전이 μ˜΅μ…˜μ΄ μ„€μ •λ˜μ–΄ μžˆλŠ” μ‚¬μš©μžμ™€ μ½˜μ„œνŠΈ ν‹°μΌ“ μ‚¬μ΄μ˜ 연관관계가 λŠμ–΄μ§€λ‹ˆ μ œκ±°λ˜λŠ” 것을 λ³Ό 수 μžˆλ‹€. 

 


 

🌱 결둠

- μ˜μ†μ„± 전이 μ˜΅μ…˜μ„(PERSIST) ν†΅ν•΄μ„œ λΆ€λͺ¨μ™€ μžμ‹μ˜ 생λͺ…μ£ΌκΈ°κ°€ 맞좰져 μžˆμ–΄μ•Ό ν•œλ‹€.

 

CascadeType.REMOVE

@OneToMany

- λΆ€λͺ¨ μ—”ν‹°ν‹° 제거 μ‹œ λΆ€λͺ¨μ™€ μžμ‹ λͺ¨λ‘ μ‹€μ œ DBμ—μ„œ μ œκ±°λœλ‹€.

- λΆ€λͺ¨ 엔티티와 μžμ‹ μ—”ν‹°ν‹°μ˜ 연관관계가 λŠμ–΄μ§€λ”λΌλ„ μ‹€μ œ DBμ—μ„œ μ œκ±°λ˜μ§€ μ•ŠλŠ”λ‹€.

 

@ManyToOne
- λΆ€λͺ¨ / μžμ‹ μ—”ν‹°ν‹° 제거 μ‹œ μ—°κ΄€λœ λΆ€λͺ¨, μžμ‹ λͺ¨λ‘ μ‹€μ œ DBμ—μ„œ μ œκ±°λœλ‹€.
- μ΄λ•Œ, μžμ‹ μ—”ν‹°ν‹°κ°€ μ—¬λŸ¬ 개일 경우 μ™Έλž˜ν‚€ μ œμ•½ 쑰건 μœ„λ°˜μœΌλ‘œ 인해 μ˜ˆμ™Έκ°€ λ°œμƒν•  수 μžˆλ‹€.
- λΆ€λͺ¨ 엔티티와 μžμ‹ μ—”ν‹°ν‹°μ˜ 연관관계가 λŠμ–΄μ§€λ”λΌλ„ μ‹€μ œ DBμ—μ„œ μ œκ±°λ˜μ§€ μ•ŠλŠ”λ‹€.


orphanRemoval = true

- λΆ€λͺ¨ μ—”ν‹°ν‹° 제거 μ‹œ λΆ€λͺ¨μ™€ μžμ‹ λͺ¨λ‘ μ‹€μ œ DBμ—μ„œ μ œκ±°λœλ‹€.
- λΆ€λͺ¨ 엔티티와 μžμ‹ μ—”ν‹°ν‹°μ˜ 연관관계가 λŠμ–΄μ§€λ©΄ μžμ‹μ€ μ‹€μ œ DBμ—μ„œ μ œκ±°λœλ‹€.

Comments