jpa의 cascade속성에 관하여 공부하던 중 문제가 발생하였다.
아래는 코드 예시들이다.
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class Team {
@Id @GeneratedValue
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
public Team(String name) {
this.name = name;
}
}
아래는 문제가 발생한 테스트 코드이다.
@Test
void hello() {
Team team = new Team("team1");
Member member = new Member("me", 20);
member.setTeam(team);
em.persist(team);
em.persist(member);
em.remove(team);
}
🤐아래 코드를 돌리면 DataIntegrityViolationException에러가 발생한다. 내용을 보면 외래키 조건에 의해 에러가 발생한다는 것이다.
이유는 간단하다. team과 member를 따로 저장해서 문제가 없는 것 같지만 자세히 보면 member의 경우는 team을 설정해 주었지만 team의 경우 member를 넣어주지 않았다.
이로 인해 team을 삭제할 경우 team은 삭제 되지만 외래키의 주인은 member이므로 member가 결정해야 한다.
그럼 어떻게 해결 해야 하는가?
🫤첫 번째 방법은 member를 삭제 하는 것이다. 외래키의 주인인 member를 삭제하고 나면 team1은 연관된 member가 없어지게 됨으로 삭제가 가능하게 된다.
😒두 번째 방법은 cascade를 all(remove도 가능)로 주는 것이다. 이렇게 되면 team 삭제시 member도 삭제가 됨으로 해결이 된다.
다만 주의할 점은 cascade를 주게되면 하위 Entity들은 cascade의 속성에 따라 연관 Entity들은 주기를 같이 하게 됨으로 정말 연관이 되어 있는 경우에만 사용하자.
그럼 두 번째 방법을 통해 설정을 해보면 아래와 같을 것이다.
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class Team {
@Id @GeneratedValue
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany(mappedBy = "team",cascade = CascadeType.ALL)
private List<Member> members = new ArrayList<>();
public Team(String name) {
this.name = name;
}
}
😎그럼 이대로 테스트를 돌리면 성공하는가?
😰돌려보면 여전히 DataIntegrityViolationException에러가 나올 것이다. 이유는 외래키 조건에 의해 삭제가 안된다는 것이다.
그러면 이 경우에는 어떻게 해결하는가?
방법은 간단하다. team.getMembers().add(member);를 통해 team에 member를 넣어주면 된다.
정말 단순하지만 중요한 점인데 그냥 위의 테스트 코드를 돌리면 현재 team에는 member가 없지만 member는 team을 연관관계를 맺게 된다.
이 상태에서 remove를 하게 되면 team은 자신을 삭제하면서 자신이 가지고 있는 member 리스트에 있는 객체들도 삭제하는데 방금 말했듯이 member 리스트에는 아무것도 없다. 이 상태에서 제거를 하려고 시도하면 현재 member는 fk키로 team1을 가지고 있는 상태여서 데이터 무결성을 위반하게 된다.
😊그러므로 항상 연관 entity들을 저장 할 때는 하나만 저장하지 말고 같이 저장하여서 객체 상태에서도 연결된 상태를 유지 하도록 하자
@Test
void hello() {
Team team = new Team("team1");
Member member = new Member("me", 20);
member.setTeam(team);
team.getMembers().add(member);
em.persist(team);
em.persist(member);
em.remove(team);
}
@Test
void hello() {
Parent parent = new Parent();
parent.setName("parent");
em.persist(parent);
Child child = new Child();
child.setName("child1");
child.setParent(parent);
em.persist(child);
em.flush();
em.clear();
Parent parent1 = em.find(Parent.class, parent.getId());
em.remove(parent1);
}
단 위 코드를 가져다 쓸 경우에는 에러가 안나고 Test class 위에 Rollback(value=false)를 붙여야만 에러가 납니다.