오늘 내용은 Cascade 입니다. DB의 그것과 비슷하면서도 완전히 다른 의미더라구요.
Cascade ("영속성 전이")
Cascade 는 아마 계단이 단계적으로 떨어지는 폭포를 의미할 겁니다 (*사전을 찾아봤지롱).
Cascade는 데이터베이스에도 있는 개념이지만, (cascade drop 이나 cascade set null 같은 거), JPA에서의 의미와는 확연히 다릅니다. 같은...건가? 아무튼 JPA에서의 Cascade JPA 오퍼레이션의 관점으로 언급되어 있습니다.
JPA의 Cascade 는 관계를 지정하는 @OneToOne, @OneToMany, @ManyToOne 등에 필드로 존재합니다.
JPA에서 Cascade가 일어나는 상황
먼저 상황을 봅시다. JPA 의 Cascade는 JPA의 개념이 녹아있는 만큼 이걸 말로만 설명하는 건 어렵거든요.
지금까지 배운 대로라면 (a) book 과 publisher를 하나씩 만들어서 (b) 둘의 관계를 엮은 뒤 (c) 둘 다 저장하는 건 다음과 같이 짜야 할 겁니다.
@SpringBootTest
class BookRepoTest {
@Autowired private BookRepo bookRepo;
@Autowired private PublisherRepo pubRepo;
@Test
void bookCascadeTest() {
// (1) book 을 만들어서 저장합니다.
Book book = new Book();
book.setName("My awesome book");
bookRepo.save(book);
// (2) publisher 를 만들어서 저장합니다.
Publisher publisher = new Publisher();
publisher.setName("sneak campus!");
pubRepo.save(publisher);
// (3) 둘의 관계를 지정해서 다시 저장합니다.
book.setPublisher(publisher);
publisher.getBooks().add(book);
bookRepo.save(book);
System.out.println(bookRepo.findAll());
System.out.println(pubRepo.findAll());
}
}
굳이 book과 publisher 를 각자 저장한 뒤, 관계를 설정해서 다시 저장하는 게 보이시나요? 불필요한 짓을 번거롭게 하고있다는 생각이 듭니다. 그냥 한 쪽에 저장하면 알아서 들어가주면 안 될까요?
오류
흐름을 끊게 되지만 오류에 대해서는 설명해야겠습니다.
failed to lazily initialize a collection of role: kr.co.fastcampus.jpasandbox.publisher.Publisher.books, could not initialize proxy - no Session
위의 코드는 오류가 납니다. 해결 방법은 @Transactional 을 붙이거나 양 관계의 필드에 모두 @ToString.Exclude 를 붙여주는 겁니다.자세한 사항은 세션과 관련있어서 이후 챕터에서 설명해주신다네요.
강의 때에는 mysql 이 안 켜져있어서 hibernate SQL favor 가 다르다드니 하는 엉뚱한 오류가 나왔다고 설명을 해주신 부분도 추가로 있었습니다.
개발자라면 감으로 알아야 읍읍
다시 원래 하던 걸로 돌아와서...
우리가 하고싶은 건 아래와 같이 (1) 저장 없이 관계만 저장한 뒤 (2) 바로 저장하면 알아서 (결과) 둘 다 저장되어주는 것입니다.
@SpringBootTest
class BookRepoTest {
@Autowired private BookRepo bookRepo;
@Autowired private PublisherRepo pubRepo;
@Test
void bookCascadeTest() {
// (1) 두 객체를 만든 뒤
Book book = new Book();
book.setName("My awesome book");
Publisher publisher = new Publisher();
publisher.setName("sneak campus!");
// (2) 저장 없이 바로 관계를 설정합니다.
book.setPublisher(publisher);
publisher.getBooks().add(book);
// (3) 저장합니다. 관계가 있는 나머지 한 쪽도 저장되도록 만들고 싶습니다.
bookRepo.save(book);
System.out.println(bookRepo.findAll());
System.out.println(pubRepo.findAll());
}
생략
물론 이 상태로 위 코드를 실행하면 오류가 나겠죠. 왜 이런진 아시죠? 0_< 한 쪽이 아직 관리되고 있지 않아서잖아요. 그쵸?
org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : kr.co.fastcampus.jpasandbox.book.Book.publisher -> kr.co.fastcampus.jpasandbox.publisher.Publisher; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : kr.co.fastcampus.jpasandbox.book.Book.publisher -> kr.co.fastcampus.jpasandbox.publisher.Publisher
여기에 JPA의 cascade 설정이 끼어들게 됩니다.
JPA의 cascade
위 코드가 동작하려면 publisher 프로퍼티의 @ManyToOne 에 cascade로 Cascade.PERSIST 를 추가해주면 됩니다. 저장할 때 달려있는 다른 객체도 같이 저장하라는 뜻이죠. JPA 입장에서는 PERSIST 연산이죠?
생략
public class Book extends 생략 {
생략
// 이거 추가
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
@ManyToOne(cascade = { CascadeType.PERSIST })
@ToString.Exclude
private Publisher publisher;
}
Hibernate:
insert into publisher 생략
Hibernate:
insert into book 생략
Hibernate:
update book set publisher_id=? where id=?
Hibernate:
select 생략 from book book0_ # <-- book0_ 은 변수 이름
[Book(super=BaseAuditingEntity(createdAt=2022-02-28T21:15:41.975665300, updatedAt=2022-02-28T21:15:41.975665300), id=1, name=My awesome book, category=null, authorId=null, publisher=Publisher(super=BaseAuditingEntity(createdAt=2022-02-28T21:15:42.003664900, updatedAt=2022-02-28T21:15:42.003664900), id=1, name=sneak campus!))]
Hibernate:
select 생략 from publisher publisher0_ # <-- publisher0_ 은 변수 이름
[Publisher(super=BaseAuditingEntity(createdAt=2022-02-28T21:15:42.003664900, updatedAt=2022-02-28T21:15:42.003664900), id=1, name=sneak campus!)]
JPA의 Persistence Context 에 Persist 할 때 연관된 객체를 같이 Persist 할 건지를 묻는 옵션입니다.
그렇다면 다른 cascade 옵션에 대해서도 손쉽게 짐작하실 수 있을 겁니다. JPA의 연산이 일어날 때 필드는 어떻게 할지를 말하는 거니까요.
기본값은 {} (아무것도 안 함, 빈 배열) 이구요.
- ALL: 어떤 연산이든 같이 반영
- PERSIST: 저장할 때
- MERGE: 업데이트할 때 (저장)
- REFRESH: 업데이트할 때 (로드)
- DETACH: Persistent Context 에서 떼어버릴 때 연결된 객체도 같이 떼버립니다.
- REMOVE: 삭제했을 때 (주의! 실수하기 쉬움! 다음 동영상에서 설명!)
각각에 대해 자세히 설명은 안 해주셨는데, 나중에 어디 공식 문서나 블로그같은 거라도 확인을 해봐야 할 것 같네요.
근데 ALL 에 {PERSIST, MERGE, REMOVE, REFRESH, DETACH} 가 다 포함되어있는데 이게 말이 되나요...? 아, detach 에 대해서 오해하고 있었나보네요. 어차피 연산이 이루어지는 대상은 필드를 가지고 있는 객체이고, 이 경우엔 Book 이 detach 될 때 Publisher 도 detach 할지를 정하는 거군요.
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC
#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online
오늘은 꽤나 직관적으로 이해할 수 있는 내용이었습니다. 원래대로라면 자료를 찾아봤어야 하는데... 몸을 사려야 하는 상황인지라. 개인 프로젝트도 해야하구요. 좀만 더 방치하면 일주일을 넘고, 일주일을 넘으면 까먹어서 그대로 버려져요...
'FastCampus - 한번에 끝내는 Java|Spring 웹 개발 > 04 JPA' 카테고리의 다른 글
JPA Ch 9 @Query (1, 2) - 패스트캠퍼스 챌린지 38일차 (0) | 2022.03.02 |
---|---|
JPA Ch 8 remove cascade / soft delete - 패스트캠퍼스 챌린지 37일차 (0) | 2022.03.01 |
JPA Ch 7 트랜젝션 매니저의 propagation - 패스트캠퍼스 챌린지 35일차 (0) | 2022.02.27 |
JPA Ch 7 영속성 - 트랜젝션 매니저 (5) - 패스트캠퍼스 챌린지 34일차 (0) | 2022.02.26 |
JPA Ch 7 영속성 - 트랜젝션 매니저 (3~4) - 패스트캠퍼스 챌린지 33일차 (0) | 2022.02.25 |