Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
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
Archives
Today
Total
관리 메뉴

개발자되기 프로젝트

Cascade Remove, Orphan Removal, Soft Delete, @Where 본문

JPA/영속성

Cascade Remove, Orphan Removal, Soft Delete, @Where

Seung__ 2021. 7. 8. 23:26
 

Cascade (영속성 전이)

1. Cascade란? 영속성 전이로, Entity의 상태(생애주기)의 변화가 있을 때 연관 관계의 entity에 상태변화(생애주기)를 전파한다. 즉 Cascade는 연관관계의 entity에 영속성을 전이시켜준다. 따라서 @OneToOne,

bsh-developer.tistory.com

 

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


 

 

bsh6463/BookManager

Contribute to bsh6463/BookManager development by creating an account on GitHub.

github.com

 

'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
Comments