2022. 7. 6. 15:27ㆍLanguage`/JPA
영속성 전이 : CASCADE
특정 엔티티를 "영속 상태"로 만들 때 → 연관된 엔티티도 함께 영속 상태로 만들고 싶다면 어떻게 해야할까
>> 영속성 전이 기능 (Transitive Persistence)
- JPA에서는 영속성 전이를 CASCADE 옵션을 통해서 제공한다
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id", nullable = false)
private Team team;
public void joinTeam(Team team){
if(this.team != null){
this.team = null;
}
this.team = team;
team.getMemberList().add(this);
}
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "team")
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
@Column(name = "team_name")
private String teamName;
@OneToMany(mappedBy = "team")
private List<Member> memberList = new ArrayList<>();
}
여기서 Team Entity를 persist하게 되면 이와 연관된 Member List들도 전부 persist하고싶어졌다
CascadeType.PERSIST
(1) 영속성 전이(CascadeType.PERSIST) 사용 X
// 팀 저장
Team team = new Team("teamA");
em.persist(team);
// memberA 저장
Member memberA = new Member("memberA");
memberA.joinTeam(team);
em.persist(memberA);
// memberB 저장
Member memberB = new Member("memberB");
memberB.joinTeam(team);
em.persist(memberB);
현재 team, memberA, memberB 전부 em.persist를 통해서 영속화 시키고 team의 List<Member>에는 memberA, memberB가 존재할 것이다
JPA에서는 엔티티를 저장할 때 "연관된 모든 엔티티는 영속 상태"이여야 한다
따라서 최종적으로 Team Entity를 저장하려면 연관된 모든 Member Entity가 영속상태이여야 한다
"만약 Member Entity가 영속상태가 아니라면 Team Entity를 저장할 때 어떤 일이 발생할까"
// team 저장 (영속화)
Team team = new Team("teamA");
em.persist(team);
// memberA (비영속)
Member memberA = new Member("memberA");
memberA.joinTeam(team);
// memberB (비영속)
Member memberB = new Member("memberB");
memberB.joinTeam(team);
당연히 Member는 저장되지 않고 team으로부터 List<Member>를 찾아도 Member가 없는 것을 확인할 수 있다
>> 여기서 "영속성 전이"를 사용하면 Team만 영속 상태로 만들면 그와 연관된 List<Member>까지 한번에 영속 상태로 만들 수 있다
2. 영속성 전이(CascadeType.PERSIST) 사용 O
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "team")
public class Team {
...
...
@OneToMany(mappedBy = "team", cascade = CascadeType.PERSIST)
private List<Member> memberList = new ArrayList<>();
}
Team의 List<Member>에 "cascade = CascadeType.PERSIST"를 설정해주면 Team과 연관된 "List<Member>에 존재하는 Member"는 Team을 persist하면 자동으로 전파되어서 persist된다
// team 저장 (영속)
Team team = new Team("teamA");
em.persist(team);
// memberA (비영속)
Member memberA = new Member("memberA");
memberA.joinTeam(team);
// memberB (비영속)
Member memberB = new Member("memberB");
memberB.joinTeam(team);
em.flush();
System.out.println("memberA 영속화? = " + em.contains(memberA));
System.out.println("memberB 영속화? = " + em.contains(memberB));
위에서 member가 저장되지 않았던 코드 그대로이다. 바뀐 것은 Team의 List<Member>에 CascadeType.PERSIST를 지정해준 것 뿐이다
그리고 em.flush()를 통해서 쿼리를 DB로 날린 후, memberA, memberB가 영속성 컨텍스트에서 영속화 되었는지 확인을 해보자
- em.flush()를 한다고 해서 영속성 컨텍스트가 clear되는 것은 "절대 아니다". 단지 SQL 저장소의 모든 쿼리가 DB로 날라갈 뿐이다
Hibernate:
/* insert cascade.domain.Team
*/ insert
into
team
(team_name)
values
(?)
Hibernate:
/* insert cascade.domain.Member
*/ insert
into
member
(name, team_id)
values
(?, ?)
Hibernate:
/* insert cascade.domain.Member
*/ insert
into
member
(name, team_id)
values
(?, ?)
memberA 영속화? = true
memberB 영속화? = true
보면 memberA, memberB를 따로 em.persist를 통해서 영속화하지 않았음에도 불구하고 영속화가 되고, 그에 따라서 DB로 insert query가 날라감을 확인할 수 있다
>> CascadeType.PERSIST로 인해서 Member들을 따로 persist하지 않아도 Team을 persist하면 그와 연관된 List<Member>까지 persist가 전파되어서 자동으로 List<Member>들도 persist됨을 확인할 수 있다
영속성 전이는 단지 "엔티티를 영속화할 때 연관된 엔티티도 자동으로 영속화"해주는 기능일뿐, 연관관계 매핑과는 전혀 관련이 없다
CascadeType.REMOVE
(1) 영속성 전이(CascadeType.REMOVE) 사용 X
CascadeType.REMOVE를 사용하지 않고 Team만 제거해버리면 어떤일이 발생할까
Team findTeam = em.find(Team.class, 1L);
em.remove(findTeam);
WARN: SQL Error: 1451, SQLState: 23000
ERROR: Cannot delete or update a parent row: a foreign key constraint fails (`jpa`.`member`, CONSTRAINT `member_ibfk_1` FOREIGN KEY (`team_id`) REFERENCES `team` (`team_id`))
Team의 FK를 보유하고 있는 Member를 지우기도 전에 Team을 remove시켜버리려고 하면 당연히 참조 무결성 제약조건을 위배하게 되어서 에러가 발생한다
따라서 team(pk = 1)을 제거하려면 먼저 해당 team에 속한 member부터 제거해준 다음에 team을 제거해야 한다
Team findTeam = em.find(Team.class, 1L);
Member findMember1 = em.find(Member.class, 1L);
Member findMember2 = em.find(Member.class, 2L);
em.remove(findMember1);
em.remove(findMember2);
em.remove(findTeam);
(2) 영속성 전이(CascadeType.REMOVE) 사용 O
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "team")
public class Team {
...
...
@OneToMany(mappedBy = "team", cascade = CascadeType.REMOVE)
private List<Member> memberList = new ArrayList<>();
}
이렇게 CascadeType.REMOVE로 설정하게 되면 Team에 대한 em.remove를 하게되면 해당 Team과 연관된 List<Member>의 Member들이 자동적으로 같이 em.remove된다
Team findTeam = em.find(Team.class, 1L);
em.remove(findTeam);
Hibernate:
/* delete cascade.domain.Member */ delete
from
member
where
member_id=?
Hibernate:
/* delete cascade.domain.Member */ delete
from
member
where
member_id=?
Hibernate:
/* delete cascade.domain.Team */ delete
from
team
where
team_id=?
영속성 전이(삭제) 설정을 해주니까 em.remove()만 해줘도 먼저 연관된 List<Member>들의 delete query가 나가게 되고 그 다음에 team에 대한 delete query가 나감을 확인할 수 있다
- FK 제약조건을 고려해서 "연관된 Member"가 먼저 삭제되고 그 다음에 Team이 삭제되는 것이다
CASCADE 종류
cascade는 여러 속성을 한번에 사용할 수 있다
@OneToMany(mappedBy = "team", cascade = {CascadeType.REMOVE, CascadeType.PERSIST})
private List<Member> memberList = new ArrayList<>();
- team을 persist하면 Member는 자동적으로 persist되고, team을 remove하면 Member도 자동적으로 remove된다
PERSIST & REMOVE는 실행할 때 바로 전이가 발생되는게 아니라 "Flush를 호출할 때" 전이가 발생한다
고아 객체 (orphanRemoval)
JPA에서는 부모 엔티티와 관계가 끊어진 자식 엔티티를 "자동으로 삭제"하는 기능을 제공해준다
Cascade.REMOVE → 부모 엔티티가 삭제되면 자동적으로 연관된 자식 엔티티들도 삭제
orphanRemoval = true → 부모 엔티티와 "관계가 끊어진" 자식 엔티티를 자동 삭제
@OneToMany(mappedBy = "team", orphanRemoval = true)
private List<Member> memberList = new ArrayList<>();
현재 Team의 List<Member>에는 {memberA, memberB, memberC}가 있다고 하자
여기서 "orphanRemoval = true"로 설정하고나서, memberC를 List에서 제거해버리면 memberC는 단순히 List에서만 제거되는게 아니라 아예 데이터 자체가 제거된다
- 그리고 orphanRemoval = true로 설정하고 부모 엔티티를 제거해버리면 개념적으로 볼 때 자식은 고아가 되기 때문에 자식도 같이 제거된다 -> CascadeType.REMOVE로 설정한 것과 같다
※ orphanRemoval = true 관련 오류
현재 orphanRemoval = true만 설정해놓고 돌리게 되면 자식 엔티티들이 삭제되지 않는 오류가 있다고 한다.
이 오류는 Hibernate측에서 2015년 쯤에 발표한 issue이고, <Hibernate 4.3.8.Final>에서는 정상동작 되었지만 추후 다른 이슈로 인해 Rollback이 되어서 다시 오류가 발생하였다고 한다
- 다른 이슈 = OneToOne 관계에서 cascade 없이 자식 엔티티를 flush할 경우 error가 발생해야 하는데 orphanRemoval = true로 지정하면 에러가 발생되지 않아서 Rollback
따라서 orphanRemoval = true를 사용하려면 CascadeType을 같이 사용해야 오류가 나지 않을 거라고 생각한다
<orphanRemoval = true만 사용>
@OneToMany(mappedBy = "team", orphanRemoval = true)
private List<Member> memberList = new ArrayList<>();
Team findTeam = em.find(Team.class, 1L);
findTeam.getMemberList().remove(0);
Hibernate:
select
team0_.team_id as team_id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.team_id=?
Hibernate:
select
memberlist0_.team_id as team_id3_0_0_,
memberlist0_.member_id as member_i1_0_0_,
memberlist0_.member_id as member_i1_0_1_,
memberlist0_.name as name2_0_1_,
memberlist0_.team_id as team_id3_0_1_
from
member memberlist0_
where
memberlist0_.team_id=?
remove를 했는데 delete query가 나가지 않는 것을 확인할 수 있다
<orphanRemoval = true + CascadeType.ALL>
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Member> memberList = new ArrayList<>();
Team findTeam = em.find(Team.class, 1L);
findTeam.getMemberList().remove(0);
Hibernate:
select
team0_.team_id as team_id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.team_id=?
Hibernate:
select
memberlist0_.team_id as team_id3_0_0_,
memberlist0_.member_id as member_i1_0_0_,
memberlist0_.member_id as member_i1_0_1_,
memberlist0_.name as name2_0_1_,
memberlist0_.team_id as team_id3_0_1_
from
member memberlist0_
where
memberlist0_.team_id=?
Hibernate:
/* delete cascade.domain.Member */ delete
from
member
where
member_id=?
원하는대로 delete query가 나가는 것을 확인하였다
orphanRemoval = true만 썻을 경우 발생하는 오류는 Hibernate측의 오류이고 JPA의 구현체중 하나인 EclipseLink를 사용하면 정상적으로 동작된다고 알려져 있습니다