'영속성전이'에 해당되는 글 2건

  1. 2019.04.29 :: Spring JPA - JPA cascade 란?!
  2. 2019.02.04 :: JPA - 영속성 전이(Cascade)와 고아 객체(Orphan)
Web/JPA 2019. 4. 29. 14:52

 

오늘 포스팅할 내용은 간단히 JPA의 cascade 기능이다. 이전 포스팅 중에 해당 내용에 대해 포스팅한적이 있지만 조금 부족한 것같아서 다시 한번 정리할겸 글을 남긴다.

 

영속성 전이(cascade)란 쉽게 말해 부모 엔티티가 영속화될때, 자식 엔티티도 같이 영속화되고 부모 엔티티가 삭제 될때, 자식 엔티티도 삭제되는 등 부모의 영속성 상태가 전이되는 것을 이야기한다. 영속성전이의 종류로는 ALL, PERSIST, DETACH, REFRESH, MERGE, REMOVE등이 있다. 이름만 봐도 어디까지 영속성이 전이되는지 확 눈에 보일 것이다. 여기서는 별도로 각각을 설명하지는 않는다.

 

오늘의 상황 : A와 B라는 엔티티가 존재하고, 두 엔티티의 관계는 @ManyToMany 관계이다. 이 관계는 중간에 Bridge 테이블을 두어 @OneToMany<->@ManyToOne  @OneToMany<->@ManyToOne 관계로 매핑하였다. 그리고 C라는 엔티티는 A엔티티와 @ManyToOne 관계이다. 이렇게 여러개가 조인이 걸려있는 상황에서 cascade를 이용한 PERSIST 예제를 한번 짜보았다.

 

<AEntity Class>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Entity
@Table(name = "TB_A")
@Getter
@Setter
@ToString
public class AEntity {
    
    @Id
    @Column(name = "A_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "SEQ_GENERATOR")
    @TableGenerator(
            name="SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="A_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=1
    )
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy="a",cascade=CascadeType.ALL)
    private List<CEntity> cList = new ArrayList<>();
    
    
    @OneToMany(mappedBy = "aEntity",fetch=FetchType.EAGER,cascade=CascadeType.ALL)
    private List<BridgeEntity> bridges;
}
 
cs

 

<BEntity class>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Entity
@Table(name = "TB_B")
@Getter
@Setter
@ToString
public class BEntity {
    @Id
    @Column(name = "B_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "SEQ_GENERATOR")
    @TableGenerator(
            name="SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="B_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=1
    )
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "bEntity")
    private List<BridgeEntity> bridges;
    
}
 
cs

 

<CEntity class>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Entity
@Table(name = "TB_C")
@Getter
@Setter
@ToString
public class CEntity {
    
    @Id
    @Column(name = "C_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "SEQ_GENERATOR")
    @TableGenerator(
            name="SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="C_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=1
    )
    private Long id;
    
    private String name;
    
    @ManyToOne
    @JoinColumn(name="A_ID", nullable=false)
    private AEntity a;
}
 
cs

 

<BridgeEntity class>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Entity
@Table(name = "TB_BRIDGE")
@Getter
@Setter
@ToString
public class BridgeEntity {
    
    @Id
    @Column(name = "BRIDGE_ID")
    @GeneratedValue(strategy=GenerationType.TABLE, generator = "SEQ_GENERATOR")
    @TableGenerator(
            name="SEQ_GENERATOR",
            table="MY_SEQUENCE",
            pkColumnName="SEQ_NAME"//MY_SEQUENCE 테이블에 생성할 필드이름(시퀀스네임)
            pkColumnValue="BRIDGE_SEQ"//SEQ_NAME이라고 지은 칼럼명에 들어가는 값.(키로 사용할 값)
            allocationSize=1
    )
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "A_ID")
    private AEntity aEntity;
    
    @ManyToOne
    @JoinColumn(name = "B_ID")
    private BEntity bEntity;
}
 
cs

 

<A,B Entity Repository class>

1
2
3
4
5
6
7
public interface AEntityRepository extends JpaRepository<AEntity, Long>{
 
}
 
public interface BEntityRepository extends JpaRepository<BEntity, Long>{
 
}
cs

 

테스트 상황 : A,B,C,BridgeEntity 모두 데이터를 persist 하는 상황이다. 그럼 여기서 생각해야 할것이 있다. 우선 C의 부모는 A이다. 즉, A 엔티티가 persist될때 C 자식 엔티티까지 persist되도록 영속성 전이기능을 이용하면 된다. 그렇다면 A,B,Bridge 세개의 관계는 어떻게 될까? 우선 조건이 있다. A,B,Bridge 관계에서 Bridge엔티티가 외래키의 주인이다. 이 말은 즉슨, Bridge 엔티티가 persist 되기 전에 A,B모두가 영속화된 상태여야하는 것이다. 예제는 아래와 같은 시나리오로 테스트했다.

 

1)B 엔티티 영속화 ->2)A 엔티티의 영속화 + Bridge 엔티티 영속성전이

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringDataJpaTestApplicationTests {
    
    
    @Autowired AEntityRepository aRepository;
    @Autowired BEntityRepository bRepository;
    
    @Test
    public void contextLoads() {
        
        AEntity a = new AEntity();
        a.setName("A");
        
        BEntity b = new BEntity();
        b.setName("B");
        
        IntStream.rangeClosed(09).forEach((i)->{
            CEntity c = new CEntity();
            c.setName(i+"");
            a.getCList().add(c);
        });
        
        a.getCList().stream().forEach((c)->{
            c.setA(a);
        });
        
        
        BridgeEntity bridge = new BridgeEntity();
        bridge.setBEntity(bRepository.save(b));
        bridge.setAEntity(a);
 
        a.setBridges(Arrays.asList(bridge));
        
        aRepository.save(a);
    }
 
}
cs

중요한 것이 있다. 부모 엔티티가 영속화되는 동시에 자식 엔티티가 영속화되게 영속성 전이를 이용하기 위해서는 반드시 자식 객체에 부모객체를 set해주어야 하는 점이다. 만약 부모엔티티를 자식 엔티티에 set해주지 않으면 외래키에 null값이 채워질 것이다.

 

결과

=>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
A_ID NAME
25     A
 
B_ID NAME
21     B
 
C_ID NAME A_ID
63     c-0  25
64     c-1  25
65     c-2  25
66     c-3  25
67     c-4  25
68     c-5  25
69     c-6  25
70     c-7  25
71     c-8  25
72     c-9  25
 
BRIDGE_ID A_ID B_ID
21          25   21
cs
posted by 여성게
:
Web/JPA 2019. 2. 4. 17:49

JPA - 영속성 전이(Cascade)와 고아 객체(Orphan)


간단히 설명하면 영속성 전이란, 연관된 엔티티가 영속화되면, 그와 연관된 엔티티까지 모두 영속화시키는것 혹은 하나의 엔티티가 영속성 컨텍스트에서 제거가 된다면, 그와 관련된 엔티티마저 영속성 컨텍스트에서 제거가 되는 것 등의 작업흐름을 영속성 전이라고한다. 즉, 데이터베이스의 Cascade와 같은 의미이다. 


고아객체란 하나의 엔티티에서 연관된 엔티티와의 참조가 끊어지면 끊어진 엔티티를 자동으로 삭제해주는 기능이다.


두개를 예제소스로 설명하겠다.






영속성 전이(Cascade = CascadeType.xxx)



우선 예제소스를 설명하기 전에 CascadeType의 종류를 나열한다면,


1
2
3
4
5
6
7
8
public enum CascadeType{
    ALL, //모두적용
    PERSIST, //영속
    MERGE, //병합
    REMOVE, //삭제
    REPRESH, //리프래쉬
    DETACH //준영속상태로 전환
}
cs



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import lombok.Getter;
import lombok.Setter;
import org.javers.core.metamodel.annotation.Entity;
 
@Entity
@Table(name = "TEAM_TB")
@Getter
@Setter
public class TeamFetchType {
    @Id
    private String id;
 
    private String name;
 
    @OneToMany(mappedBy="team",cascade = CascadeType.ALL)
    private List<MemberFetchTypeLazy> members = new ArrayList<MemberFetchTypeLazy>();
}
 
public class CascadeTest {
    //엔티티매니저 팩토리 생성
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
    //엔티티매니저 생성
    EntityManager em = emf.createEntityManager();
    public void create() {
        //트랜잭션 획득
        EntityTransaction tx = em.getTransaction();
 
        try {
            tx.begin();
 
            TeamFetchType team = new TeamFetchType();
            team.setId("team_1");
            team.setName("team_1");
            /*em.persist(team);*/
 
            MemberFetchTypeLazy member = new MemberFetchTypeLazy();
            member.setId("member_1");
            member.setName("윤여성");
            member.setTeam(team);
 
            team.getMembers().add(member);
 
            em.persist(team);
 
            tx.commit();
 
            em.clear();
        }catch (Exception e1) {
            // TODO: handle exception
            tx.rollback();
        }finally {
 
        }
    }
 
    public void remove() {
        //트랜잭션 획득
        EntityTransaction tx = em.getTransaction();
 
        try {
            tx.begin();
 
            TeamFetchType findTeam = em.find(TeamFetchType.class"team_1");
 
            em.remove(findTeam);
 
            tx.commit();
        }catch (Exception e1) {
            // TODO: handle exception
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();
    }
 
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        CascadeTest t = new CascadeTest();
        t.create();
        t.remove();
 
        System.out.println();
    }
}
cs

위의 소스에서 CascadeType.ALL로 모든 영속성 전이 속성을 허용하였다. Team엔티티 인스턴스를 만들고 Member엔티티 인스턴스를 만든 다음에 Team엔티티에 add(member)를 했다.(반드시 영속성 속성이 정의된 엔티티에 객체를 add해야한다. 그래야 Team엔티티를 persist할때 Team필드에 add된 Member까지 persist된다.) 그리고 em.persist()호출하니 Team엔티티는 물론 Member엔티티까지 데이터베이스에 insert 됬다. 그리고 위 remove()에서 em.remove(findTeam)을 호출하는 순간 Team 엔티티가 삭제됨과 동시에 Member 엔티티도 모두 삭제되는 것을 볼 수 있다. 만약 영속성 전이 속성을 사용하지 않았다면 Team 엔티티를 삭제하는 순간 외래키 참조무결성 조약에 어긋나서 예외가 발생할 것이다. 즉, 영속성 전이 속성없이 외래키의 주인인 Member를 지우는 것은 문제가 안되지만 이미 Member가 참조하고 있는 Team을 삭제하는 순간 문제가 되는 것이다. 그냥 쉽게 생각하면 데이터베이스에 Cascade 키워드를 사용하는 것이라고 생각하면 된다.






고아객체(orphan)



위에서도 간단히 설명했지만 고아객체란 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제되는 기능이다.


소스에서도 보듯이 findTeam.getMembers().clear() 메소드를 호출했을 뿐인데, 플러쉬 단계에서 해당 Member엔티티를 삭제한다. 즉, 고아객체 속성이 적용된 하나의 엔티티에서 연관관계에 있는 엔티티의 참조를 제거하는 것만으로 연관된 엔티티가 삭제되는 것이다.


하지만 주의할 점은 고아객체 속성은 반드시 참조가 제거된 엔티티가 어디에서든 참조하지 않는 객체일때만 사용해야한다. 만약 다른데에서도 참조하는 엔티티인데 삭제가 된다면 문제가 생길 수 있다.

posted by 여성게
: