λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°
Spring/Spring Data JPA

JPA 연관관계 μ˜μ†μ„± 전이(CASCADE) - CascadeType

by 주발2 2021. 6. 21.
λ°˜μ‘ν˜•

πŸ“Ž  JPA μ˜μ†μ„± 전이(CASCADE)

μ•ˆλ…•ν•˜μ„Έμš”! μ΄λ²ˆμ— 정리할 λ‚΄μš©μ€ JPAμ—μ„œ μ˜μ†μ„± 전이(CASCADE)와 κ΄€λ ¨λœ λ‚΄μš©μž…λ‹ˆλ‹€.

졜근 κ°œλ°œμ„ μ§„ν–‰ν•˜λ©° 두 μ—°κ΄€λœ μ—”ν‹°ν‹°μ—μ„œ save()λ₯Ό ν•˜λŠ” κ³Όμ •μ—μ„œ 였λ₯˜κ°€ λ°œμƒν–ˆκ³ ,

이λ₯Ό ν•΄κ²°ν•˜λŠ” κ³Όμ •μ—μ„œ μ˜μ†μ„± 전이(CASCADE)λΌλŠ” κ°œλ…μ— λŒ€ν•΄ μ•Œκ²Œ λ˜μ—ˆκ³ , 이λ₯Ό μ •λ¦¬ν•˜κ³ μž ν•©λ‹ˆλ‹€. 😊

(β€» 잘λͺ»λœ λ‚΄μš©μ΄ ν¬ν•¨λ˜μ–΄ μžˆμ„μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. 잘λͺ»λœ 뢀뢄에 λŒ€ν•΄ μ§€μ ν•΄μ£Όμ‹œλ©΄ κ°μ‚¬ν•˜κ² μŠ΅λ‹ˆλ‹€ πŸ˜‚)


 

 

 

🎯  μ˜μ†μ„±μ „μ΄(CASCADE)λž€ ?

λΆ€λͺ¨ μ—”ν‹°ν‹°κ°€ μ˜μ†ν™”λ  λ•Œ μžμ‹ 엔티티도 같이 μ˜μ†ν™”λ˜κ³ , λΆ€λͺ¨ μ—”ν‹°ν‹°κ°€ μ‚­μ œλ  λ•Œ μžμ‹ 엔티티도 μ‚­μ œλ˜λŠ” λ“± νŠΉμ • μ—”ν‹°ν‹°λ₯Ό μ˜μ† μƒνƒœλ‘œ λ§Œλ“€ λ•Œ μ—°κ΄€λœ 엔티티도 ν•¨κ»˜ μ˜μ† μƒνƒœλ‘œ μ „μ΄λ˜λŠ” 것을 μ˜λ―Έν•©λ‹ˆλ‹€.

 

https://www.baeldung.com/jpa-cascade-types

즉, νŠΉμ • 엔티티에 λŒ€ν•΄ νŠΉμ •ν•œ μž‘μ—…μ„ μˆ˜ν–‰ν•˜λ©΄ κ΄€λ ¨λœ 엔티티에도 λ™μΌν•œ μž‘μ—…μ„ μˆ˜ν–‰ν•œλ‹€λŠ” μ˜λ―Έμž…λ‹ˆλ‹€.

(해석 λ§žλ‚˜μš”? γ…Žγ…Ž;)

 

 

JPAμ—μ„œλŠ” μ΄λŸ¬ν•œ μ—°κ΄€λœ μ—”ν‹°ν‹°κ°„μ˜ μ˜μ‘΄μ„±(dependency)을 μ„€μ •ν•˜κΈ° μœ„ν•΄ Enum νƒ€μž…μ˜ javax.persistence.CascadeType 을 μ œκ³΅ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

/*
 * Copyright (c) 2008, 2019 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Linda DeMichiel - 2.1
//     Linda DeMichiel - 2.0

package javax.persistence;

/**
 * Defines the set of cascadable operations that are propagated 
 * to the associated entity.
 * The value <code>cascade=ALL</code> is equivalent to 
 * <code>cascade={PERSIST, MERGE, REMOVE, REFRESH, DETACH}</code>.
 *
 * @since 1.0
 */
public enum CascadeType { 

    /** Cascade all operations */
    ALL, 

    /** Cascade persist operation */
    PERSIST, 

    /** Cascade merge operation */
    MERGE, 

    /** Cascade remove operation */
    REMOVE,

    /** Cascade refresh operation */
    REFRESH,

    /**
     * Cascade detach operation
     *
     * @since 2.0
     * 
     */   
    DETACH
}

 

CascadeTypeλŠ” μ½”λ“œμ™€ 같이 PERSIST, MERGE, DETACH, PREFRESH, REMOVE, ALL 둜 κ΅¬μ„±λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.

 

 

각각의 operations에 λŒ€ν•œ μ„€λͺ…은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  • CascadeType.ALL: λͺ¨λ“  Cascadeλ₯Ό 적용
  • CascadeType.PERSIST: μ—”ν‹°ν‹°λ₯Ό μ˜μ†ν™”ν•  λ•Œ, μ—°κ΄€λœ 엔티티도 ν•¨κ»˜ μœ μ§€
  • CascadeType.MERGE: μ—”ν‹°ν‹° μƒνƒœλ₯Ό 병합(Merge)ν•  λ•Œ, μ—°κ΄€λœ 엔티티도 λͺ¨λ‘ 병합
  • CascadeType.REMOVE: μ—”ν‹°ν‹°λ₯Ό μ œκ±°ν•  λ•Œ, μ—°κ΄€λœ 엔티티도 λͺ¨λ‘ 제거
  • CascadeType.DETACH: λΆ€λͺ¨ μ—”ν‹°ν‹°λ₯Ό detach() μˆ˜ν–‰ν•˜λ©΄, μ—°κ΄€ 엔티티도 detach()μƒνƒœκ°€ λ˜μ–΄ λ³€κ²½ 사항 반영 X
  • CascadeType.REFRESH: μƒμœ„ μ—”ν‹°ν‹°λ₯Ό μƒˆλ‘œκ³ μΉ¨(Refresh)ν•  λ•Œ, μ—°κ΄€λœ 엔티티도 λͺ¨λ‘ μƒˆλ‘œκ³ μΉ¨

 

 

 

 

πŸ€”  문제 상황

μ„œλ‘ μ—μ„œ μ–ΈκΈ‰ν–ˆλ“―μ΄, μ—°κ΄€λ˜μ–΄ μžˆλŠ” 두 μ—”ν‹°ν‹°μ—μ„œ save()λ₯Ό μ§„ν–‰ν•˜λŠ” κ³Όμ •μ—μ„œ 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.

 

ν˜„μž¬ μ—°κ΄€λœ μ—”ν‹°ν‹°μ˜ μ½”λ“œλŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

@Entity
@Getter
@ToString(exclude = "member")
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Board extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long bno;

    @Column
    private String title;

    @Column
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    private Member member;

    public void changeTitle(String title) {
        this.title = title;
    }

    public void changeContent(String content) {
        this.content = content;
    }
}



import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
@Getter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member extends BaseEntity {

    @Id
    private String email;

    @Column
    private String password;

    @Column
    private String name;
}

Board(κ²Œμ‹œκΈ€) κ³Ό Member(νšŒμ›) 은 N:1의 관계λ₯Ό 가지고 μžˆμŠ΅λ‹ˆλ‹€.

 

 

λ˜ν•œ Boardμ—μ„œ @ManyToOne μ–΄λ…Έν…Œμ΄μ…˜μ„ 톡해 Member μ—”ν‹°ν‹°λ₯Ό 가지고 μžˆμŠ΅λ‹ˆλ‹€.

 

μœ„ 두 μ—”ν‹°ν‹°λ₯Ό 톡해 save()λ₯Ό ν•˜λŠ” μ½”λ“œλŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

    @Test
    @DisplayName("연관관계λ₯Ό 가진 두 엔티티에 CASCADE 속성을 μ„€μ •ν•˜μ§€ μ•ŠμœΌλ©΄ μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.")
    void insertBoard() {
        IntStream.rangeClosed(301, 350)
                .forEach(i -> {
                    Member member = Member.builder().email("user" + i + "@aaa.com").build();
                    Board board = Board.builder()
                            .title("Title..." + i)
                            .content("Content..." + i)
                            .member(member)
                            .build();

                    boardRepository.save(board);
                });
    }

μœ„ ν…ŒμŠ€νŠΈμ½”λ“œλ₯Ό μ‹€ν–‰ν•˜λ©΄ λ‹€μŒκ³Ό 같은 μ—λŸ¬κ°€ λ°œμƒν•©λ‹ˆλ‹€.

 

 

 

Caused by: java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing ...

 

Caused by: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing ...

 

 

https://docs.jboss.org/hibernate/orm/5.1/javadocs/org/hibernate/TransientPropertyValueException.html

TransientPropertyValueException ν΄λž˜μŠ€μ— λŒ€ν•œ μ„€λͺ…μž…λ‹ˆλ‹€.

λŒ€λž΅μ μΈ μ˜λ―ΈλŠ” μΌμ‹œμ μœΌλ‘œ μ €μž₯λ˜μ§€ μ•Šμ€ μ—”ν‹°ν‹°μ™€μ˜ κ΄€κ³„λ‘œ 인해 persist(μ˜μ†ν™”) ν•  수 μ—†μ„λ•Œ μ˜ˆμ™Έλ₯Ό λ˜μ§„λ‹€κ³  μ„€λͺ…ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

 

 

 

 

πŸ€”  ν•΄κ²° 방법

λ”°λΌμ„œ μ—”ν‹°ν‹°μ˜ 연관관계에 CASCADE 섀정을 ν•΄μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€.(CascadeType)

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

@Entity
@Getter
@ToString(exclude = "member")
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Board extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long bno;

    @Column
    private String title;

    @Column
    private String content;

    // μ˜μ†μ„± 전이(Cascade)
    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
    private Member member;

    public void changeTitle(String title) {
        this.title = title;
    }

    public void changeContent(String content) {
        this.content = content;
    }
}

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
@Getter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member extends BaseEntity {

    @Id
    private String email;

    @Column
    private String password;

    @Column
    private String name;
}

ν˜„μž¬ 제 μ½”λ“œμ—μ„œλŠ” Board μ—”ν‹°ν‹°λ§Œ Member μ—”ν‹°ν‹°λ₯Ό 가지고 μžˆμŠ΅λ‹ˆλ‹€.

λ”°λΌμ„œ Board의 @ManyToOne μ–΄λ…Έν…Œμ΄μ…˜μ— μ˜μ†μ„± 전이 μ„€μ •(CascadeType.PERSIST)을 μΆ”κ°€ν•©λ‹ˆλ‹€.

 

 

κ·Έ ν›„ ν…ŒμŠ€νŠΈμ½”λ“œλ₯Ό λ‹€μ‹œ 싀행해보면 μ •μƒμ μœΌλ‘œ μž‘λ™μ΄ λ©λ‹ˆλ‹€.

    @Test
    @DisplayName("연관관계λ₯Ό 가진 두 엔티티에 CASCADE 속성을 μ„€μ •ν•˜λ©΄ μ •μƒμ μœΌλ‘œ save()λ©”μ†Œλ“œκ°€ μˆ˜ν–‰λœλ‹€.")
    void insertBoard() {
        IntStream.rangeClosed(351, 400)
                .forEach(i -> {
                    Member member = Member.builder().email("user" + i + "@aaa.com").build();
                    Board board = Board.builder()
                            .title("Title..." + i)
                            .content("Content..." + i)
                            .member(member)
                            .build();

                    boardRepository.save(board);
                });
    }

 

 

 

 

Preferences

 

λ°˜μ‘ν˜•

λŒ“κΈ€