일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- Spring Boot
- 자바
- Servlet
- transaction
- QueryDSL
- 인프런
- Proxy
- JPQL
- 스프링 핵심 원리
- Thymeleaf
- http
- Android
- JDBC
- SpringBoot
- springdatajpa
- pointcut
- Greedy
- java
- db
- spring
- jpa
- 김영한
- 스프링 핵심 기능
- 그리디
- 백준
- Exception
- kotlin
- AOP
- 스프링
- 알고리즘
- Today
- Total
개발자되기 프로젝트
Cascade Remove, Orphan Removal, Soft Delete, @Where 본문
1. Cascade REMOVE
ALL | 아래 속성 모두 포함. 항상 영속성 전이. |
PERSIST | 부모 persist시 연관 entity도 persist |
MERGE | Transaction종료 후 detach 상태에서 연관 entity 수정/추가 후 부모 entity가 merge()를 하면 연관 entity의 변경, 추가내용 저장됨. |
REMOVE | 삭제 시 연관 entity도 삭제 |
REFRESH | entity를 다시 로드했을 때, 연관관계 entity도 같이 로드 |
DETATCH | 영속성 관리하지 않겠다. detatch 시점에 함께 detach |
Test에서 Book entity를 삭제해 보자.
아래와 같이 작성해주자.
<BookRepositoryTest>
@Test
public void bookCascadeTest(){
Book book = new Book();
book.setName("JPA 공부하자");
Publisher publisher = new Publisher();
publisher.setName("패스트캠");
book.setPublisher(publisher);
bookRepository.save(book);
System.out.println("books : " + bookRepository.findAll());
System.out.println("publishers : " + publisherRepository.findAll());
Book book1 = bookRepository.findById(1L).get();
book1.getPublisher().setName("탐사수");
bookRepository.save(book1);
System.out.println("publishers : " + publisherRepository.findAll());
Book book2 = bookRepository.findById(1L).get();
bookRepository.delete(book2);
System.out.println("books : " + bookRepository.findAll());
System.out.println("publishers : " + publisherRepository.findAll());
}
테스트 결과 입력된 book 삭제를 완료했다.
books : []
publishers : [Publisher(super=BaseEntity(createdAt=2021-07-08T20:09:57.778800, u
pdatedAt=2021-07-08T20:09:58.350853), id=1, name=탐사수)]
이 전 글에서 영속성 전이에 대해 학습을 했다.
Cascade가 Persist인 경우 부모 enitty가 Persist될 때 해당 entity도 Persist가 되었다.
반면에 부모 entity가 삭제되면 해당 entity도 삭제 하려면 어떻게 해야할까???
bookRepository.delete(book2);
publisherRepository.delete(book2.getPublisher());
직접 repository에서 삭제가 가능하긴 하다.
또는 Cascade에 REMOVE를 적용하면 된다.
<Book>
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE})
@ToString.Exclude
private Publisher publisher;
이전에 만들어둔 data.sql에 book과 publisher를 추가해주자.
insert into publisher(id, name) values (1, '탐사수');
insert into book(id, name, publisher_id) values (1, '탐사수수수수', 1);
insert into book(id, name, publisher_id) values (2,'삼다수', 1)
잘 입력이 되었는지 확인해보자.
@Test
void bookRemoveCascadeTest(){
System.out.println("books : " + bookRepository.findAll());
System.out.println("publishers : " + publisherRepository.findAll());
}
엇? book과 publisher에 연관관계를 볼 수 가 없다..?
books : [
Book(super=BaseEntity(createdAt=null, updatedAt=null), id=1, name=탐사수수수수,
authorId=null, category=null),
Book(super=BaseEntity(createdAt=null, updatedAt=null), id=2, name=삼다수,
authorId=null, category=null)]
publishers : [Publisher(super=BaseEntity(createdAt=null, updatedAt=null), id=1, name=탐사수)]
순환참조를 끊기 위해 @ToString.Exclude를 적용했기 때문!
<Book>
@OneToMany
@JoinColumn(name = "book_id")
@ToString.Exclude
private List<BookAndAuthor> bookAndAuthors = new ArrayList<>(); //NPE방지하기위해 array생성
public void addBookAndAuthors(BookAndAuthor ... bookAndAuthors){ //배열로 받겠다.
Collections.addAll(this.bookAndAuthors, bookAndAuthors); //book 정보가 여러개 들어오면 한꺼번에 저장.
}
그렇다면 직접 불러와보자.
@Test
void bookRemoveCascadeTest(){
System.out.println("books : " + bookRepository.findAll());
System.out.println("publishers : " + publisherRepository.findAll());
bookRepository.findAll().forEach(book -> System.out.println(book.getPublisher()));
}
각 book entity에 대하여 publisher를 불어왔다!
흠 근데? createdAt, updatedAt이 null이다? 해당 entity들은 data.sql에 바로 입력을 해준 값이다.
data.sql은 단순 query를 실행시키기만 한다. 따라서 listener가 작동이 안되었다 ㅎㅎ
books : [Book(super=BaseEntity(createdAt=null, updatedAt=null), id=1, name=탐사수수수수, authorId=null, category=null), Book(super=BaseEntity(createdAt=null, updatedAt=null), id=2, name=삼다수, authorId=null, category=null)]
publishers : [Publisher(super=BaseEntity(createdAt=null, updatedAt=null), id=1, name=탐사수)]
Publisher(super=BaseEntity(createdAt=null, updatedAt=null), id=1, name=탐사수)
Publisher(super=BaseEntity(createdAt=null, updatedAt=null), id=1, name=탐사수)
자 book이 있는 것을 확인했으니 삭제해보자!
현재 cascade는 PERSIST, MERGE, REMOVE가 적용되어있다.(상위 : BOOK)
@Test
void bookRemoveCascadeTest(){
bookRepository.deleteById(1L);
System.out.println("books : " + bookRepository.findAll());
System.out.println("publishers : " + publisherRepository.findAll());
bookRepository.findAll().forEach(book -> System.out.println(book.getPublisher()));
}
의도한 대로 상위 entity인 book이 삭제되자 publisher도 삭제가 되었다.
books : [Book(super=BaseEntity(createdAt=null, updatedAt=null), id=2, name=삼다수, authorId=null, category=null)]
publishers : []
null
3. Orphan Removal
부모 엔티티(publisher)에서 자식 entity에 대한 relation을 삭제하는 경우 자식 enitity(book)를 db에서 삭제함
publisher : book --> 1 : N 관계
연관관계가 없는 entity를 제거하는 속성
연관 관계 끊기 : setter를 통해 null 주입
먼저 data.sql에서 마지막에 추가해준 book, publisher를 삭제
id가 1인 book을 가져와서 publisher에 null을 주입해보자.
@Test
public void bookCascadeTest(){
Book book = new Book();
book.setName("JPA 공부하자");
Publisher publisher = new Publisher();
publisher.setName("패스트캠");
book.setPublisher(publisher);
bookRepository.save(book);
System.out.println("books : " + bookRepository.findAll());
System.out.println("publishers : " + publisherRepository.findAll());
Book book1 = bookRepository.findById(1L).get();
book1.getPublisher().setName("탐사수");
bookRepository.save(book1);
System.out.println("publishers : " + publisherRepository.findAll());
Book book2 = bookRepository.findById(1L).get();
//bookRepository.delete(book2);
//publisherRepository.delete(book2.getPublisher());
Book book3 = bookRepository.findById(1L).get();
book3.setPublisher(null);
bookRepository.save(book3);
System.out.println("books : " + bookRepository.findAll());
System.out.println("publishers : " + publisherRepository.findAll());
System.out.println("book3 - publisher : " + bookRepository.findById(1L).get().getPublisher());
}
짠 book3의 publisher에 null을 주입했더니 relation이 사라졌다. 당연함...
book3 - publisher : null
하지만 그렇다고 publisher가 삭제된 것은 아니다.
publishers : [Publisher(super=BaseEntity(createdAt=2021-07-08T20:42:19.112611, updatedAt=2021-07-08T20:42:19.461699), id=1, name=탐사수)]
요 때 orphan removal을 사용한다. @OneToMany의 옵션으로 설정이 가능하다!
<Publisher>
@OneToMany(orphanRemoval = true)
@JoinColumn(name = "publisher_id")
@ToString.Exclude
private List<Book> books = new ArrayList<>();
<Test>
@Test
void orphanTest(){
Book book = new Book();
book.setName("자식");
Publisher publisher = new Publisher();
publisher.setName("부모");
book.setPublisher(publisher);
publisher.addBook(book);
bookRepository.save(book);
publisherRepository.save(publisher);
//부모 entity에서 자식 entity삭제
System.out.println(publisher.getBooks().get(0));
publisher.getBooks().remove(0);
publisherRepository.save(publisher);
System.out.println("books : " + bookRepository.findAll());
System.out.println("book-publisher : " + bookRepository.findById(1L).get().getPublisher());
System.out.println("publishers : " + publisherRepository.findAll());
}
부모 엔티티(publisher)에서 자식 entity(book)의 연관관계를 삭제하니
book에서도 마찬가지로 publisher와 연관관계를 삭제했다.
근데 delete쿼리가 생성되지 않는다.... flush를 강제로 시켜도 달라지지 않는다....ㅜㅜ
Hibernate:
update
publisher
set
created_at=?,
updated_at=?,
name=?
where
id=?
Hibernate:
update
book
set
publisher_id=null
where
publisher_id=?
and (
deleted = false
)
----------------------------------------------------------------------------------------------------------------------------------
HIBERNATE 구현체에 버그가 있다고 한다.
orphanRemoval을 사용할 때, Cascade.PESIST나 ALL을 함께 사용해 주어야 한다.
따라서 부모 entity인 publisher에서 아래와 같이 추가해준다.
@OneToMany(cascade = CascadeType.PERSIST, orphanRemoval = true)
@JoinColumn(name = "publisher_id")
@ToString.Exclude
private List<Book> books = new ArrayList<>();
테스트는 아래와 같이 진행해주자.
book, publisher를 생성하고 연관관계를 맺어주었다.
그 다음, publisher(부모)에서 book에 대한 연관관계를 삭제했다.
@Test
void orphanTest(){
Book book = new Book();
book.setName("자식");
Publisher publisher = new Publisher();
publisher.setName("부모");
book.setPublisher(publisher);
publisher.addBook(book);
bookRepository.save(book);
publisherRepository.save(publisher);
//부모 entity에서 자식 entity삭제
publisher.getBooks().remove(0);
publisherRepository.save(publisher);
System.out.println("books : " + bookRepository.findAll());
System.out.println("publishers : " + publisherRepository.findAll());
}
}
실행 결과 publisher와 book 모두 삭제가 되었다.. 이것은 의도한 바가 아니다.
그 원인은 Book에서 Publisher에 대해 cascade.REMOVE를 적용했기 때문이다.
publisher에서 book에 대한 연관관계를 삭제하면, orphan removal에 의해 book이 삭제되고
cascade에 의해 다시 publisher가 삭제된 경우이다.
따라서 book에서 publisher에대한 cascade.REMOVE를 지워준다.
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@ToString.Exclude
private Publisher publisher;
테스트 실행 결과 relation이 삭제된 자식 entity인 book이 delete 되었다!
Hibernate:
delete
from
book
where
id=?
books : []
publishers : [Publisher(super=BaseEntity(createdAt=2021-07-08T23:57:27.333564,
updatedAt=2021-07-08T23:57:27.450377), id=1, name=부모)]
만약 publisher와 여러개의 book과 연관관계가 있다면??
book2를 생성하여 publisher와 연관관계 맺은 뒤 publisher에서 book만 연관관계를 삭제해봤다.
그 결과 book2, publisher는 그대로 존재했다.
* Cascade.REMOVE와 Orphan Removal의 차이점
1) Cascade의 REMOVE는 상위 Entity가 삭제될 때 하위 Entity도 삭제 되는 것 이다.
즉, Relation이 삭제되는 상황에서 적용이 되지 않는다.
book 과 publisher가 N:1의 관계일 때
<Book>
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE})
@ToString.Exclude
private Publisher publisher;
book을 remove하면 하면 publisher도 remove가 되지만.
book.serPublisher(null) 을 하는 경우(relation삭제) publisher를 삭제하지는 못한다.
즉, setOrders(null)을 했을 경우 DB에서 삭제되면 안된다 = remove cascade
DB에서 삭제되어야 한다 = orphan removal
4. soft delete
일반적으로 상용 서비스는 db레코드를 delete 쿼리를 사용하는 경우는 많이 없다. flag를 사용한다.
회원탈퇴의 경우가 아닌 이상..
예를들어 deleted라를 flag가 있다고 하자.
private boolean deleted;
deleted가 true일 때 지워진 상태, false이면 지워지지 않은 상태이다.
문제는, 이런 flag를 사요할때, 일반적인 조회에서 true값이 존재하면 안된다.
<data.sql>에 다음과 같이 book, publisher를 추가하고 deleted flag도 추가한다.
insert into publisher(id, name) values (1, '탐사수');
insert into book(id, name, publisher_id, `deleted`) values (1, '탐사수수수수', 1, false);
insert into book(id, name, publisher_id, `deleted`) values (2,'삼다수', 1, false);
insert into book(id, name, publisher_id, `deleted`) values (3, '백산수', 1, true);
Test에서 bookRepository에서 book을 전부 불러와보자.
@Test
void softDelete(){
bookRepository.findAll().forEach(System.out::println);
}
flag에 관계없이 불러와진다.
Book(super=BaseEntity(createdAt=null, updatedAt=null),
id=1, name=탐사수수수수, authorId=null, category=null, deleted=false)
Book(super=BaseEntity(createdAt=null, updatedAt=null),
id=2, name=삼다수, authorId=null, category=null, deleted=false)
Book(super=BaseEntity(createdAt=null, updatedAt=null),
id=3, name=백산수, authorId=null, category=null, deleted=true)
flag값에 따라 검색결과를 변경하기 위해서는 Repository에서 새로 선언해 줘야한다.
find~~~DeletedFalse를 붙여줘야 삭제되지 않은 data가 조회된다.
<BookRepository>
List<Book> findByDeletedFalse();
List<Book> findByCategoryIsNullAndDeletedFalse();
<Test>
@Test
void softDelete(){
bookRepository.findAll().forEach(System.out::println);
//bookRepository.findByCategoryIsNull().forEach(System.out::println);
bookRepository.findByDeletedFalse().forEach(System.out::println);
}
짠 findByDeletedFalse에 의해 deleted가 false인 data만 조회되었다.
Book(super=BaseEntity(createdAt=null, updatedAt=null), id=1, name=탐사수수수수, authorId=null, category=null, deleted=false)
Book(super=BaseEntity(createdAt=null, updatedAt=null), id=2, name=삼다수, authorId=null, category=null, deleted=false)
Book(super=BaseEntity(createdAt=null, updatedAt=null), id=3, name=백산수, authorId=null, category=null, deleted=true)
Book(super=BaseEntity(createdAt=null, updatedAt=null), id=1, name=탐사수수수수, authorId=null, category=null, deleted=false)
Book(super=BaseEntity(createdAt=null, updatedAt=null), id=2, name=삼다수, authorId=null, category=null, deleted=false)
이렇게 항상 ~DeletedFalse를 붙여줘야 한다.
넘무 귀찮다. 근데 빼먹으면 망한다.ㅎㅎㅎ
5. @Where
이 때 사용하는 것이 @Where Annotation이다.
일반적으로 soft delete에서 사용이 되며, 기본 query 들에 항상 붙어서 조건을 추가해준다.
즉, @Where(Clause = "deleted = false")인 경우 아래와 같이 반영되는 것이다.
findAll() --> findAllByDeletedFalse()
findById() -->findByIDDeletedFalse()
<Book>
@Where(clause = "deleted = false")
public class Book extends BaseEntity {
~~~~~
}
<Test>
bookRepository.findAll().forEach(System.out::println);
<결과> 그냥 findAll()을 했는데, deleted= false인 경우만 불러왔다!
Book(super=BaseEntity(createdAt=null, updatedAt=null),
id=1, name=탐사수수수수, authorId=null, category=null, deleted=false)
Book(super=BaseEntity(createdAt=null, updatedAt=null),
id=2, name=삼다수, authorId=null, category=null, deleted=false)
6. GitHub
'JPA > 영속성' 카테고리의 다른 글
@Transactional을 Interface에 적용한 경우 (0) | 2021.07.10 |
---|---|
Transaction Manager - 5 : Propagation (0) | 2021.07.06 |
Transaction Manager - 4 : Isolation (0) | 2021.07.05 |
Transaction Manager-3 : Isolation (0) | 2021.07.04 |
Transaction Manager - 2 (0) | 2021.07.04 |