[Spring] Cascade 및 고아제거속성

WOOK JONG KIM·2022년 11월 22일
0

패캠_java&Spring

목록 보기
62/103
post-thumbnail

CasCade(영속성 전이)

영어로는 작은 폭포를 의미 -> 단계별로 전이가 발생

해당 영역으로 전이가 일어날 시 포함하고 있는 릴레이션 엔티티에도 함께 전이를 일으킬지 설정하는 값

@OneToOne, @OneToMany, @ManyToOne 연관 관계가 있는 어노테이션에 cascade() 지원

CascadeType.java 에는 ALL, PERSIST, MERGE,REMOVE, REFRESH, DETACH속성 지원

  • ALL → 모든 Cascade 적용
  • PERSIST → 엔티티를 영속화할 때, 연관된 엔티티도 함께 유지
  • MERGE → 엔티티 상태를 병합(Merge)할 때, 연관된 엔티티도 모두 병합
  • REMOVE → 엔티티를 제거할 때, 연관된 엔티티도 모두 제거(주의해야할 옵션)
  • REFRESH → 상위 엔티티를 새로고침(Refresh)할 때, 연관된 엔티티도 모두 새로고침
  • DETACH → 부모 엔티티를 detach() 수행하면, 연관 엔티티도 detach()

JPA는 자바코드를 SQL쿼리로 번역해주는 ORM
-> save() 즉 영속화를 자주 사용함으로서 자바관점에선 객체지향적인 코드가 아니다

		@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); 
            
			// call by reference(자바) , setter 가 직관적, 따라서 add Book 메서드 생성하는 것이 좋음
        	// publisher.getBook().add(book);
        
		    publisher.addBook(book); //Publisher Entity에 영속화된 Book 수정
		    publisherRepository.save(publisher);
		
		    System.out.println("books : " + bookRepository.findAll());
		    System.out.println("publishers : " + publisherRepository.findAll());
		}

위 코드는 디비에 저장, 디비에 저장 후 연관관계를 맺어 주었음

영속성 전이를 이용해 객체 중심의 코드 수정이 가능

Book.java

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

위 코드의 의미: Book을 persist할때 연관성이 잇는 publisher도 같이 persist 해라

@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());
        
        --- 이까지 CasCadeType : Persist로 해결 ---

        Book book1 = bookRepository.findById(1L).get();
        book1.getPublisher().setName("남의 출판사");

        bookRepository.save(book1);

        System.out.println("publishers : " + publisherRepository.findAll());
        
        --- merge 안할 시 이름이 변경이 안됨 ---
        --- update 시 merge에 대한 이벤트가 발생하여 이를 위한 영속성 전이를 해주어야 함 ---
    }
}
books : [Book(super=BaseEntity(createdAt=2022-11-22T16:49:09.267504, updatedAt=2022-11-22T16:49:09.267504), id=1, name=jpa 공부하기, category=null, authorId=null)]
publishers : [Publisher(super=BaseEntity(createdAt=2022-11-22T16:49:09.271674, updatedAt=2022-11-22T16:49:09.271674), id=1, name=나는출판사)]
publishers : [Publisher(super=BaseEntity(createdAt=2022-11-22T16:49:09.271674, updatedAt=2022-11-22T16:49:09.349512), id=1, name=남의 출판사)]

Default 값으로는 아무런 Cascade가 일어나지 않음


고아 제거 속성

Book.java

@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;

		...
}

book만 제거하더라도 연관관계에 있는 Publisher 까지 같이 제거됨

@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()));
    }
}

하지만 book과 Publisher는 N:1 관계-> 하나의 출판사가 여러가지 책 만듬

data.sql(위 상황에 이용)

insert into publisher(`id`, `name`) values (1, '패트스캠퍼스');

insert into book(`id`, `name`, `publisher_id`) values(1, 'JPA 초격차 패키지', 1);

insert into book(`id`, `name`, `publisher_id`) values(2, '스프링 시큐리티 패키지', 1);

이대로 각각의 리포지토리 프린트시 ToString.Exclude 때문에 연관관계는 표시 안됨

연관관계가 없는 엔티티를 제거하는 속성

연관관계를 제거하는 방법은 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) 설정

Cascade는 말그대로 상위 객체가 리무브 액션을 취하고있는 포함하고 잇는 객체에 해당 영속성 이벤트를 전파해서 하위 엔티티까지 리무브 시켜주는것

-> 연관관계가 끊어질때(set null) 리무브 이벤트가 발생하지는 않음

연관관계가 없는 데이터를 살려둘려면 orphanRemoval = false 사용
-> 고아 객체 제거하려면 true

공식 문서

For orphan removal : If you invoke setOrders(null), the related Order entities will be removed in the db automatically

For remove Cascade : f you invoke setOrders(null), the related Order entities will Not be removed in db automatically

Soft Delete

현업에서 자주 사용하는 방법

이 전 방법들은 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();
}

test 코드

@SpringBootTest
class BookRepositoryTest {	
		...	
		@Test
    void softDelete() {
        bookRepository.findAllByDeletedFalse().forEach(System.out::println);
        bookRepository.findByCategoryIsNullAndDeletedFalse().forEach(System.out::println);
    }
}

하지만 이 경우 DeletedFalse를 누락할 시 큰 Side Effect를 불러 올 수 있음


이 경우 Entity에 @Where를 추가하고 조건문 생성

@Where(clause = "deleted = false")
public class Book extends BaseEntity{
	...
}

작성한 조건문은 항상 추가되어 쿼리문이 실행 됨

Hibernate: 
    select
        book0_.id as id1_2_,
        book0_.created_at as created_2_2_,
        book0_.updated_at as updated_3_2_,
        book0_.author_id as author_i4_2_,
        book0_.category as category5_2_,
        book0_.deleted as deleted6_2_,
        book0_.name as name7_2_,
        book0_.publisher_id as publishe8_2_ 
    from
        book book0_ 
    where
        (
            book0_.deleted = 0
        )
Book(super=BaseEntity(createdAt=null, updatedAt=null), id=1, name=JPA 초격차 패키지, category=null, authorId=null, deleted=false)
Book(super=BaseEntity(createdAt=null, updatedAt=null), id=2, name=스프링 시큐리티 패키지, category=null, authorId=null, deleted=false)

자동으로 id=3의 값은 추가되지 않음

DB에는 값이 존재하지만 로직상에서 출력 되지 않게 하는 방법!!

profile
Journey for Backend Developer

0개의 댓글