백엔드/JPA

[ JPA ] 7. 영속성 전이 (Cascade)

Reference. 한 번에 끝내는 Java/Spring 웹 개발 마스터 초격차 패키지 Online

이전 글

1. Cascade (영속성 전이)

  • 영어로는 폭포를 의미하며 단계별로 전이가 발생
  • @OneToOne, @OneToMany, @ManyToOne 연관 관계가 있는 어노테이션에 cascade() 지원
  • CascadeType.java 에는 ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH속성 지원
    • DETACH: 준영속(detach)으로 변경할 때 연관 엔티티도 준영속
    • REFRESH: 엔티티를 다시 로드할 때, 연관 엔티티도 재로드
    • ALL: 모든 경우에 연관 엔티티 전파
  • JPA는 자바코드를 SQL쿼리로 번역해주는 ORM
    • save()를 자주 사용함으로 자바관점에선 불필요한 코드 사용(save는 영속화 시키기 위함)
    @SpringBootTest
    class BookRepositoryTest {
    		@Autowired
        private BookRepository bookRepository;
        @Autowired
        private PublisherRepository publisherRepository;
    		...
    		
    		@Test
    		void bookCascadeTest(){
    		    Book book = new Book();
    		    book.setName("JPA 책");
    		    bookRepository.save(book); //Book Entity 영속화
    		
    		    Publisher publisher = new Publisher();
    		    publisher.setName("테스트 출판사");
    		    publisherRepository.save(publisher); //Publisher Entity 영속화
    		
    		    book.setPublisher(publisher); //Book Entity에 영속화된 Publisher 수정
    		    bookRepository.save(book); 
    		
    		    publisher.addBook(book); //Publisher Entity에 영속화된 Book 수정
    		    publisherRepository.save(publisher);
    		
    		    System.out.println("books : " + bookRepository.findAll());
    		    System.out.println("publishers : " + publisherRepository.findAll());
    		}
    }
    • 영속성 전이를 이용해 객체 중심의 코드 수정이 가능
      • 엔티티를 영속화하지 않아도 엔티티간 관계 설정이 가능 ( save() )
      • @ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) 로 연관관계 적용
      • PERSIST(등록), MERGE(수정) 등을 할 때 영속성 전이가 발생
    ...
    public class Book extends BaseEntity{
    		...
    		@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
        @ToString.Exclude
        private Publisher publisher;
    }
    @SpringBootTest
    class BookRepositoryTest {
    		...
    		@Test
        void bookCascadeTest(){
            Book book = new Book();
            book.setName("JPA 책");
    
            Publisher publisher = new Publisher();
            publisher.setName("테스트 출판사");
    
    				//비영속화 상태에서 엔티티간 연관관계 맺기가 안됌 (cascade로 연관관계 맺음)
            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());
        }
    }
    //실행결과
    books : [Book(super=BaseEntity(createdAt=2021-08-08T19:01:18.537728, updatedAt=2021-08-08T19:01:18.537728), id=1, name=JPA 책, category=null)]
    publishers : [Publisher(super=BaseEntity(createdAt=2021-08-08T19:01:18.588729, updatedAt=2021-08-08T19:01:18.588729), id=1, name=테스트 출판사)]
    
    publishers : [Publisher(super=BaseEntity(createdAt=2021-08-08T19:01:18.588729, updatedAt=2021-08-08T19:01:19.107600), id=1, name=변경된 출판사)]

2. 삭제처리, 고아제거 속성

cascade 삭제

Book.java (CascadeType.REMOVE 추가)
@Entity
@NoArgsConstructor
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Book extends BaseEntity{
		...

		@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE})
    @ToString.Exclude
    private Publisher publisher;

		...
}
BookRepositoryTest.java
@SpringBootTest
class BookRepositoryTest {		
		@Autowired
    private BookRepository bookRepository;
    @Autowired
    private PublisherRepository publisherRepository;
		...		

		@Test
    void bookRemoveCascadeTest(){
        bookRepository.deleteById(1L); //cascade 삭제속성으로 연관엔티티 관계제거

        System.out.println("books : " + bookRepository.findAll());
        System.out.println("publishers : " + publisherRepository.findAll());

        bookRepository.findAll().forEach(book -> System.out.println(book.getPublisher()));
    }
}

고아제거 속성

  • 연관관계가 없는 엔티티를 제거하는 속성
  • 연관관계를 제거하는 방법은 setter에 null값을 주입
    Hibernate: 
        update
            book_and_author 
        set
            book_id=null 
        where
            book_id=?
    
    Hibernate: 
        update
            review 
        set
            book_id=null 
        where
            book_id=?
    
    Hibernate: 
        update
            book 
        set
            publisher_id=null 
        where
            publisher_id=?
    
    Hibernate: 
        delete 
        from
            book 
        where
            id=?
    
    Hibernate: 
        delete 
        from
            publisher 
        where
            id=?
  • CascadeType.REMOVE, orphanRemoval의 특징
    • 설정하는 방법 (엔티티 속성)
      • CascadeType.REMOVE: @ManyToOne(cascade = {CascadeType.REMOVE}) 설정
      • orphanRemoval: @OneToMany(orphanRemoval = true) 설정
    • 동작의 차이
      • CascadeType.REMOVE:
        • 상위객체의 remove 이벤트를 하위 엔티티에 영속성 이벤트를 전파하여 삭제
        • setter를 통해 null을 실행하면 관련된 엔티티가 삭제 안됌 (데이터 보존)
      • orphanRemoval: setter를 통해 null을 실행하면 관련된 엔티티가 삭제

소프트 delete 처리 (flag 사용)

  • 상용화 된 시스템에는 delete 삭제를 하지 않고 삭제 flag 컬럼을 생성 후 조건으로 검색
  • 아래의 예시는 deleted flag 컬럼을 설정하고 true는 삭제로 판단
    • java boolean타입은 db에 0과 1로 표시 (0: false, 1: true)
data.sql
...

insert into book(`id`, `name`, `publisher_id`, `deleted`) values(1, 'JPA 초격자 패키지', 1, false);
insert into book(`id`, `name`, `publisher_id`, `deleted`) values(2, 'Spring', 1, false);
insert into book(`id`, `name`, `publisher_id`, `deleted`) values(3, 'Spring Security', 1, true);
Book.java
...
public class Book extends BaseEntity{
		...

		private boolean deleted;
}
BookRepository.java
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
		...

		List<Book> findAllByDeletedFalse();

    List<Book> findByCategoryIsNullAndDeletedFalse();
}
BookRepositoryTest.java
@SpringBootTest
class BookRepositoryTest {	
		...	
		@Test
    void softDelete() {
        bookRepository.findAllByDeletedFalse().forEach(System.out::println);
        bookRepository.findByCategoryIsNullAndDeletedFalse().forEach(System.out::println);
    }
}
//findAllByDeletedFalse
Hibernate: 
    select
        book0_.id as id1_2_,
        book0_.created_at as created_2_2_,
        book0_.updated_at as updated_3_2_,
        book0_.category as category4_2_,
        book0_.deleted as deleted5_2_,
        book0_.name as name6_2_,
        book0_.publisher_id as publishe7_2_ 
    from
        book book0_ 
    where
        book0_.deleted=0

Book(super=BaseEntity(createdAt=null, updatedAt=null), id=1, name=JPA 초격자 패키지, category=null, deleted=false)
Book(super=BaseEntity(createdAt=null, updatedAt=null), id=2, name=Spring, category=null, deleted=false)


//findByCategoryIsNullAndDeletedFalse
Hibernate: 
    select
        book0_.id as id1_2_,
        book0_.created_at as created_2_2_,
        book0_.updated_at as updated_3_2_,
        book0_.category as category4_2_,
        book0_.deleted as deleted5_2_,
        book0_.name as name6_2_,
        book0_.publisher_id as publishe7_2_ 
    from
        book book0_ 
    where
        (
            book0_.category is null
        ) 
        and book0_.deleted=0

Book(super=BaseEntity(createdAt=null, updatedAt=null), id=1, name=JPA 초격자 패키지, category=null, deleted=false)
Book(super=BaseEntity(createdAt=null, updatedAt=null), id=2, name=Spring, category=null, deleted=false)
  • repository에 매번 메소드를 추가하는 것은 불편하고 다른 버그가 발생할 확률 다수
  • Entity@Where를 추가하고 조건문 생성
    @Where(clause = "deleted = false")
    public class Book extends BaseEntity{
    	...
    }