JPA에서의 영속성 전이(Transitive Persistence)
Transitive라는것은 '이곳에서 저곳으로 옮겨진다'는 뉘앙스입니다.
영속성(Persistence)가 전이된다는 것은 무슨 의미일까요?
JPA에서 영속성 전이(Transitive Persistence)라는 것은 다음과 같은 의미입니다.
"영속성 컨텍스트에 의해서 관리되지 않던 엔티티가
영속성 컨텍스트에 의해 관리되는 상태로 변화한다"
당연히 이렇게 결론만 들어서는 무슨 말인지 이해하기가 쉽지 않습니다. 바로 예시를 보겠습니다.
먼저 다음과 같이 두 개의 엔티티를 정의하도록 해보겠습니다.
먼저 저는 데이터베이스에 member와 team이라는 것이 다음과 같은 형태로 저장이 되기를 바랍니다.
그래서 Member라는 엔티티와 Team이라는 엔티티를 다음과 같이 정의했습니다.
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@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<>();
...
}
부모 엔티티와 자식 엔티티
이 때 여기서 부모 엔티티는 Team, 자식 엔티티는 member가 됩니다.
부모 엔티티와 자식 엔티티라는 건 객체지향 프로그래밍의 상속에서 나오는 부모-자식 개념과 조금 다른데,
엔티티에서 부모 자식은 데이터 생성 시점을 기준으로 합니다.
가령 이 상황에서는 팀이라는 것이 먼저 존재 하고, 멤버들이 이미 존재해 있는 팀에 속하는 형태의 관계인데, 여기서 먼저 독립적으로 존재하는 엔티티인 팀을 부모 엔티티, 이 엔티티로부터 파생되어 존재하는 엔티티를 자식 엔티티라고 이해하시면 됩니다. 자식 엔티티(member)에서 부모 엔티티(team)을 참조하는 형태가 됩니다.
여기서 teamA이라는 팀에 속하는 멤버를 저장해 보도록 하겠습니다.
@SpringBootTest
@Transactional
@Rollback(value = false)
public class MyCustomizedTest {
@PersistenceContext
EntityManager em;
@Test
public void test() {
Team teamA = new Team("teamA");
Member member1 = new Member("member1", 10);
member1.setTeam(teamA);
em.persist(member1);
}
}
member1이라는 이름을 가진 열 살짜리 멤버를 하나 생성해 줍니다.
그리고 이 멤버가 속한 팀을 teamA로 설정해서 영속 상태로 넣어준다는 아주 간단한 내용의 코드입니다.
그런데 이를 실행하면 바로 다음과 같은 오류가 발생합니다.
org.hibernate.TransientPropertyValueException:
object references an unsaved transient instance - save the transient instance before flushing : study.datajpa.entity.Member.team -> study.datajpa.entity.Team
왜 이런 오류가 발생할까요? 왜 이런 오류가 발하는 지를 알기 위해서는 다음 사실을 알아야 합니다.
"특정 엔티티(Member)를 영속상태로 만들기 위해서는,
연관된 엔티티(Team)들이 이미 영속상태에 있어야 한다."
여기서 member1이라는 엔티티 객체의 참조변수로 teamA라는 객체를 가지고 있습니다.
그런데 이 teamA라는 객체는 생성만 되었지, 영속화되지는 않은 객체입니다.
member1이라는 객체를 영속화 하려고 보았더니, 이와 관련된 객체인 teamA가 아직 영속화되지 않았기 때문에 바로 예외(TransientPropertyValueException)가 발생했던 것입니다.
그래서 위와 같은 코드를 작성하려면 다음과 같이 해야 합니다.
@Test
public void test() {
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10);
Member member2 = new Member("member2", 20);
Member member3 = new Member("member3", 30);
member1.setTeam(teamA);
member2.setTeam(teamB);
member3.setTeam(teamA);
em.persist(member1);
em.persist(member2);
em.persist(member3);
}
먼저 참조의 대상이 되는 엔티티를 위와 같이 먼저 영속화를 시킨 후(persist)에,
이 엔티티를 참조하는 엔티티도 마저 영속화를 시켜야(persist)합니다.
Cascade를 이용해 부모 엔티티와 관련된 모든 자식 엔티티를 한번에 영속화
다음과 같이 부모 엔티티에 cascade option을 붙여보겠습니다.
public class Team {
...
@OneToMany(mappedBy = "team", cascade = CascadeType.PERSIST)
private List<Member> members = new ArrayList<>();
...
}
이렇게 cascade = CascadeType.PERSIST라는 옵션을 붙였는데 이 옵션의 의미는 다음과 같습니다.
"Team엔티티를 영속화할 때에
이 엔티티의 내부에 컬렉션 형태로 담긴 모든 member 엔티티까지
한 번에 영속화를 한다."
그러면 다음과 같이 member에 대한 persist를 호출하지 않아도,
각 팀에 속한 회원들이 자동으로 영속화가 됩니다.
@Test
public void test() {
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
Member member1 = new Member("member1", 13);
Member member2 = new Member("member2", 26);
Member member3 = new Member("member3", 24);
member1.setTeam(teamA);
member2.setTeam(teamB);
member3.setTeam(teamA);
// 이 부분이 중요!
teamA.getMembers().add(member1);
teamB.getMembers().add(member2);
teamA.getMembers().add(member3);
// 이후에 member를 따로 영속화하지 않아도
// team 엔티티만 영속화하면 자동으로 member도 영속화 진행
em.persist(teamA);
em.persist(teamB);
}
데이터베이스에도 다음과 같이 모든 member가 잘 저장된 것을 볼 수가 있습니다.
Cascade를 이용해 부모 엔티티와 관련된 모든 자식 엔티티를 한번에 제거
다음과 같이 cascade.remove옵션을 설정하면
부모 엔티티를 삭제 시,
삭제된 부모 엔티티가 컬렉션 형태로 참조하고 있는 모든 자식 엔티티까지 한꺼번에 삭제할 수가 있습니다.
(반드시 컬렉션 형태로 참조하고 있어야 합니다.)
public class Team {
...
//다음과 같이 여러 cascade option을 동시에 사용할 수가 있습니다.
@OneToMany(mappedBy = "team", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
private List<Member> members = new ArrayList<>();
...
}
다음의 코드를 한번 실행해 보겠습니다.
@Test
public void test() {
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 13);
Member member2 = new Member("member2", 26);
Member member3 = new Member("member3", 24);
member1.setTeam(teamA);
member2.setTeam(teamB);
member3.setTeam(teamA);
teamA.getMembers().add(member1);
teamB.getMembers().add(member2);
teamA.getMembers().add(member3);
// CascadeType.REMOVE의 사용 결과
// teamA를 삭제하면 그와 관련된 member1과 member3도 함께 삭제되어야 한다.
em.remove(teamA);
}
마지막 부분이 포인트인데, teamA를 삭제하면 이 부모 엔티티 객체가 컬렉션 형태로 참조하고 있는 member1과 member3도 동시에 삭제되어야 합니다.
실행 결과는 다음과 같습니다.
CascadeType.REMOVE를 사용하니 부모엔티티 삭제 시, 이 부모엔티티가 참조하는 모든 자식엔티티까지 모두 삭제된 것을 볼 수가 있습니다.
'Back-End > Spring' 카테고리의 다른 글
왜 Spring Security 의존성을 추가 하기만 해도 모든 엔드포인트에 인증과정이 요구되는가 (0) | 2024.10.22 |
---|---|
[디버깅] Spring Security 사용 중 템플릿에 CSS가 적용되지 않는 현상 (0) | 2024.07.18 |
[디버깅] Maven으로 빌드한 스프링부트 프로젝트에서 템플릿 파일 인식 (0) | 2024.07.18 |
데이터베이스의 엔티티(Entity)에 대한 고찰, 그리고 JPA의 @Entity 어노테이션 (0) | 2024.04.12 |
Spring Data JPA에서의 Transaction 및 수정 작업 (0) | 2024.04.09 |