문제 상황
여행 계획을 저장하는 api를 구현하기 위해서 최초에 entity 설계를 Post, Days, Places로 나눠서 구상했습니다.
예를 들어 유저가 여행(Post) 중에 2일차(Days)에는 어느 장소들(Places)을 방문할 예정인지를 파악하기 위해 앞서 언급한 것 처럼 entity를 나눈 것입니다.
프론트에서 여행 계획 저장 정보가 들어오는 모습은 다음과 같았습니다.
Request
{
"postUUID" :"1hrrlmg8w0bod",
"startDate" : "2022-04-28",
"endDate" : "2022-04-30",
"dateCnt" : 3,
"postTitle" : "경주 여행",
"location" : "경주",
"theme" : "액티비티",
"islike" : false,
"ispublic" :true,
"days" :
[
{
"places":[
{
"placeName" : "혜화",
"placeInfoUrl" : "url",
"category" : "한식 > 고기집",
"address": "주소",
"roadAddress" : "연돈로",
"placeMemo" : "맛있대요",
"lat" :23525325,
"lng" : 235235235,
"planTime": 1500
},
{
"placeName" : "장소명2",
"placeInfoUrl" : "url2",
"category" : "한식 > 고기집2",
"address": "주소2",
"roadAddress" : "도로명 주소2",
"placeMemo" : "메모2",
"lat" :235325235,
"lng" : 235235325,
"planTime": 1100
}
]
}
... places 반복으로 생략 ...
]
}
이에 따라 가장 하위 엔티티인 places 부터 forEach문으로 돌려서 각 장소 정보를 저장하고 그 장소들의 상위 엔티티인 days가 마찬가지로 반복문을 돌며 1일차, 2일차 날짜를 저장했습니다. 마지막으로 이 모든 것이 save 됐을 때 post도 최종 수정이 되도록 코드를 짰습니다.
POST 엔티티
@NoArgsConstructor
@Getter
@Setter
@Entity
public class Post extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long postId;
@Column(nullable = false /*, unique = true*/)
private String postUUID;
... 생략 ...
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
@OneToMany
@JoinColumn(name = "POST_ID")
private List<Days> days = new ArrayList<>();
}
---------------------------------------------------------
DAYS 엔티티
@Entity
@Getter
@NoArgsConstructor
public class Days {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long dayId;
@Column(nullable = false)
private int dateNumber;
@OneToMany
@JoinColumn(name = "DAYS_ID")
private List<Places> places = new ArrayList<>();
...생략...
}
}
--------------------------------------------------------
PLACES 엔티티
@Entity
@Getter
@NoArgsConstructor
public class Places {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long placeId;
@Column(nullable = false)
private String placeName;
@Column(nullable = true, length = 500)
private String placeInfoUrl;
... 생략...
그리고 나서 실행을 시키고 테스트를 해봤는데 에러가 발생했습니다.
org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing :
@OneToMany로 관계를 설정해주었으니 설정한대로 데이터베이스에 잘 저장될 줄 알았지만 위 에러를 찾아보니 Fk로 쓰는 객체가 아직 저장이 안 돼서 영속성 오류가 발생한 것이었습니다.
해결
Cascade를 적용시켜서 연관된 엔티티들과의 영속성을 유지할 수 있는 방법이 있었습니다.
CascadeType.PERSIST
엔티티를 생성하고, 연관 엔티티를 추가한 상태로 영속화할 때 연관 엔티티도 함께 persist()가 수행된다.
CascadeType.MERGE
트랜잭션이 종료되고 detach상태에서 연관 엔티티를 추가하거나 변경된 이후에 부모 엔티티가 merge를 수행하게 되면 변경사항이 연관 엔티티에도 적용된다.
CascadeType.REFRESH
엔티티를 새로고칠 때, 이 필드에 보유된 엔티티도 새로고친다.
CascadeType.REMOVE
엔티티를 삭제할 때, 이 필드에 보유된 엔티티도 삭제한다.
CascadeType.DETACH
부모 엔티티가 detach()를 수행하게 되면, 연관된 엔티티도 detach() 상태가 되어 변경사항이 반영되지 않는다.
CascadeType.ALL
모든 Cascade 적용
출처: https://dev-coco.tistory.com/90 [슬기로운 개발생활😃:티스토리]
여기서 어떤 타입을 사용해야할까 고민이었는데, 현재 영속성이 유지되지 않고 있어서 나는 에러라고 판단이 되어 연관 엔티티의 영속성을 유지해주는 PERSIST를 적용시켰습니다.
원하는 대로 Fk에 잘 매핑되어 정보가 저장되는 것을 확인했습니다.
However, 문제가 이대로 끝나지 않았습니다.
post를 하나 지우게 되면 지금과 같이 연관되어 있는 days와 places 정보들은 같이 삭제가 되어야 합니다.
PERSIST가 연관 엔티티와 영속성을 유지해주는 타입이라고 생각해서 삭제 시에도 같이 삭제될 것이라고 판단했지만 정보의 저장에만 관여를 하고 삭제에서는 영속성 유지에 관여하지 않았습니다.
수정
이에 따라 CascadeType을 변경해야하는 소요가 발생했고, PERSIST와 REMOVE 타입이 둘다 필요했습니다. 결과적으로 CascadeType.ALL을 채택해 정보 저장 시 영속성 유지는 물론 삭제 시 연관 엔티티까지 삭제하는데에 성공했습니다.
POST 엔티티
@NoArgsConstructor
@Getter
@Setter
@Entity
public class Post extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long postId;
@Column(nullable = false /*, unique = true*/)
private String postUUID;
... 생략 ...
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "POST_ID")
private List<Days> days = new ArrayList<>();
}
---------------------------------------------------------
DAYS 엔티티
@Entity
@Getter
@NoArgsConstructor
public class Days {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long dayId;
@Column(nullable = false)
private int dateNumber;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "DAYS_ID")
private List<Places> places = new ArrayList<>();
...생략...
}
}
--------------------------------------------------------
PLACES 엔티티
@Entity
@Getter
@NoArgsConstructor
public class Places {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long placeId;
@Column(nullable = false)
private String placeName;
@Column(nullable = true, length = 500)
private String placeInfoUrl;
... 생략...
참고
https://dev-coco.tistory.com/90
https://velog.io/@devsh/JPA-CASCADE-%EC%98%81%EC%86%8D%EC%84%B1-%EC%A0%84%EC%9D%B4