티스토리 뷰

카테고리 없음

7.2 @OneToMany

parkrams 2023. 7. 14. 13:45
728x90

7.2 @OneToMany

  • @ManyToOne도 가능하지만 @OneToMany 이용하는 경우 추가 작업 없이 필요한 여러 종류 엔티티 객체처리가 장점

OneToMany 적용

  • 게시물과 댓글, 게시물과 첨부파일의 관계를 테이블 구조로 보면 완전히 같은 구조지만 이를 JPA에서는 게시글을
    중심으로 해석하는지, 첨부파일을 중심으로 해석하는지에 따라 결과가 다르다
  • 기본적으로 상위 엔티티(게시물)와 여러 개의 하위 엔티티들(첨부파일)의 구조로 이루어진다,
  • ManyToOne과 다른 점은 ManyToOne은 다른 엔티티 객체의 참조로 FK를 가지는 쪽에서 하는 방식이고, OneToMany는 PK를 가진 쪽에서 사용한다는 점
  • @OneToMany를 사용하는 구조
    • 상위 엔티티에서 하위 엔티티 관리
    • JPA Repository를 상위 엔티티 기준으로 생성. 하위 엔티티 변경이 상위 엔티티에도 반영
    • 상위 엔티티 상태가 변경되면 하위 엔티티도 같이 처리
    • 상위 엔티티 하나와 하위 엔티티 여러개를 처리하는 경우 'N+1' 문제가 발생할 수 있으므로 주의

BoardImage 클래스의 생성

  • 첨부파일을 의미하는 BoardImage 엔티티 클래스를 domain 패키지에 선언하고, @ManyToOne 연관 관계를 적용
  • BoardIm age는 첨부파일의 고유한 uuid 값과, 파일의 이름, 순번을 지정하고, @ManyToOne으로 Board 객체를 지정
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "board")
public class BoardImage implements Comparable<BoardImage>{

    @Id
    private String uuid;

    private String fileName;

    private int ord;

    @ManyToOne
    private Board board;

    @Override
    public int compareTo(BoardImage other){
        return this.ord - other.ord;
    }

    public void changeBoard(Board board){
        this.board = board;
    }
}
  • BoardImage에는 특이하게도 Comparable 인터페이스를 적용하는데 이는 @OneToMany 처리에서 순번에 맞게 정렬하기 위함
  • BoardImage에는 changeBoard()를 이용해 Board 객체를 나중에 지정할 수 있게 해주는데 이는 나중에 Board 엔티티 삭제 시 BoardImage 객체의 참조도 변경하기 위해 사용
Board 클래스에 @OneToMany 적용
  • BoardImage에 대한 참조를 가지는 방식으로 구조를 작성
    //board 클래스에 @OneToMany 적용
    @OneToMany
    @Builder.Default
    private Set<BoardImage> imageSet = new HashSet<>();
테이블 생성 확인과 mappedBy
  • @OneToMany는 기본적으로 각 엔티티에 해당하는 테이블을 독립적으로 생성하고 중간에 매핑해 주는 테이블이 생성
  • 확인을 위해 board 테이블과 raply 테이블을 삭제
  • board_image_set 이 @OneToMany를 처리하기 위해 생성

mappedBy를 이용한 구조 변경

  • 엔티티 테이블 사이에 생성되는 테이블을 '매핑 테이블' 이라고 한다
  • 매핑 테이블을 생성하지 않는 방법으로 단방향 @OneToMany를 이용하는 경우 @JoinColumn을 이용하거나 mappedBy라는 속성을 이용하는 방법 존재
  • mappedBy의 경우 Board와 BoardImage가 서로 참조를 유지하는 양방향 참조 상황애서 사용
    • mappedBy는 '어떤 엔티티의 속성으로 매핑되는지'를 의미

mappedBy : '연관 관계의 주인'

  • Board 클래스의 연관관계 수정
    // mappedBy 사용
    @OneToMany(mappedBy = "board") // BoardImage의 board 변수
    @Builder.Default
    private Set<BoardImage> imageSet = new HashSet<>();

영속성의 전이 (cascade)

  • 상위 엔티티(Board)와 하위 엔티티(BoardImage)의 연관 관계를 상위 엔티티에서 관리하는 경우 신경써야 하는 가장 중요한 점 중 하나는 상위 객체의 상태가 변경되었을 때 하위 객체도 영향을 받는다는 점
  • JPA에서 'cascade'라는 용어로 이를 표현 하는데, 가장 대표적인 영속성의 전의가 바로 지금부터 작성하게 되는 Board와 BoardImage의 저장
  • BoardImage 가 JPA에 의해 관리 되면 BoardImage를 참조하는 Board 객체도 같이 처리 되어야 함
  • 반대도 존재

JPA에서는 이러한 경우 연관 관계에 cascade 속성을 부여해 이를 제어하도록 한다

Board와 BoardImage의 insert 테스트

  • 현재 구조에서 BoardImage는 Board가 저장될 때 같이 저장되어야 하는 엔티티 객체
  • 상위 엔티티가 하위 엔티티 객체들을 관리하는 경우 별도의 JPARepository를 생성하지 않고, Board 엔티티에 하위 엔티티 객체들을 관리하는 기능을 추가해 사용
  • entity package Board.class 에 코드 추가
// mappedBy 사용
    @OneToMany(mappedBy = "board",             // BoardImage의 board 변수
            cascade = {CascadeType.ALL},
            fetch = FetchType.LAZY
    )
    @Builder.Default
    private Set<BoardImage> imageSet = new HashSet<>();


 // 이미지 추가
    public void addImage(String uuid, String fileName){

        BoardImage boardImage = BoardImage.builder()
                .uuid(uuid)
                .fileName(fileName)
                .board(this)
                .ord(imageSet.size())
                .build();
        imageSet.add(boardImage);
    }

    // 삭제?
    public void clearImages(){
        imageSet.forEach(boardImage -> boardImage.changeBoard(null));

        this.imageSet.clear();
    }
  • @OneToMany의 cascade 속성값으로 CascadeType.ALL을 지정해 Board 엔티티 객체의 모든 상태 변화에 BoardImage 객체들 역시 같이 변경되도록 구성
  • Board 객체 자체에서 BoardImage 객체를 관리하도록 addImage()와 clearImages()를 이용
  • addImage()는 내부적으로 BoardImages 객체 내부의 Board에 대한 참조를 this를 이용해 처리
  • clearImages()는 첨부파일을 모두 삭제하므로 BoardImage 객체의 Board 참조를 null로 변경

Lazy로딩과 @EntityGraph

  • @OneToMany의 로딩 방식은 기본적으로 지연(lazy) 로딩.
  • 게시물을 조회하는 경우 Board 객체와 BoardImage 객체들을 생성해야 하므로 2번의 select가 필요
  • 테스트 코드로 확인
Hibernate: 
    select
        board0_.bno as bno1_0_0_,
        board0_.moddate as moddate2_0_0_,
        board0_.regdate as regdate3_0_0_,
        board0_.content as content4_0_0_,
        board0_.title as title5_0_0_,
        board0_.writer as writer6_0_0_ 
    from
        board board0_ 
    where
        board0_.bno=?

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.board.domain.Board.imageSet, could not initialize proxy - no Session
  • 이 에러를 해결하는 가장 간단한 방법은 @Transactional을 추가
    • @Transactional을 적용하면 필요할 때 마다 추가적인 쿼리를 여러번 실행하는 것이 가능

EntityGraph와 조회 테스트

  • 하위 엔티티를 로딩하는 가장 간단한 방법은 즉시(eager)로딩을 적용하는 것이지만 가능하면 지연(lazy)로딩을 이용하는 것이 기본적인 방식이므로 조금 특별한 @EntityGraph를 이용
  • 지연 로딩이라고 해도 한 번에 조인 처리해서 select가 이루어지도록 하는 방법을 이용
  • BoardRepository에 findByIdWithImages()를 정의

    // EntityGraph 에 attributePath 를 이용해 같이 로딩해야하는 속성을 명시 
    @EntityGraph(attributePaths = {"imageSet"})
    @Query("select b from Board b where b.bno =:bno")
    Optional<Board> findByIdWithImages(Long bno);

    -- > //Java 8 이상의 버전을 사용하는 경우, javac 컴파일러 플래그 -parameters를 사용하는 방법:
Java 8 이상의 버전을 사용하고 있다면, 컴파일 시점에 메소드 파라미터의 이름을 유지하기 위해 javac 컴파일러 플래그 -parameters를 추가해야 합니다.

    //바꾼 코드
  -->>    Optional<Board> findByIdWithImages(@Param("bno") Long bno); 
 - 
  이렇게 @Param 을 지정해줘야 에러가 발생하지 않는다.


}
  • 이렇게 하면 board와 boardImage가 join 처리 된 상태로 select가 실행 되면서 Board와 BoardImage를 한번에 처리

게시물과 첨부파일 수정

  • 게시물과 첨부파일 수정은 다른 엔티티들 간의 관계와는 조금 다른 점이 존재
  • 실제 처리 과정에서 첨부파일은 그 자체가 변경되는 것이 아니라 아예 기존의 모든 첨부파일이 삭제되고 새로운 첨부파일이 추가
  • Board에는 addImage()와 clearImage()를 이용해 Board를 통해 BoardImage 객체들을 처리하도록 설계되어있다.
orphanRemoval 속성
  • 테스트 코드로 특정 게시물의 첨부파일을 다른 파일들로 수정
  • 테스트코드를 통해 실행해보면 영향은 주지만 삭제가 되지 않는다
  • cascade 속성이 ALL로 지정 되어 있어 상위 엔티티(Board)의 상태 변화가 하위 엔티티(BoardImage) 영향을 주지만 삭제는 안됨
  • 하위 엔티티의 참조가 더 이상 없는 상태가 되면 @OneToMany에 orphanRemoval 속성값을 true로 지정해주면 실제 삭제가 이루어짐

게시물과 첨부파일 삭제

  • 게시물 삭제는 게시물을 사용하는 댓글을 먼저 삭제 해야 한다. 다른 사용자가 만든 데이터를 삭제하는 것은 문제가 되니 주의 필요
  • ReplyRepository에 특정 게시물에 해당하는 데이터를 삭제할 수 있도록 쿼리 메소드 추가
    //게시물과 첨부파일 삭제
    void deleteByBoard_Bno(Long bno);
  • Test 코드 작성
  // 게시물 댓글 같이 삭제 테스트
    @Test
    @Transactional
    @Commit
    public void testRemoveAll(){

        Long bno = 1L;

        replyRepository.deleteByBoard_Bno(bno);

        boardRepository.deleteById(bno);


    }
  • 처리 결과
// 댓글이 존재하는 경우 댓글 삭제
Hibernate: 
    delete 
    from
        reply 
    where
        rno=?


// 이미지 첨부파일이 존재하는 경우 삭제 
Hibernate: 
    delete 
    from
        board_image 
    where
        uuid=?
Hibernate: 
    delete 
    from
        board_image 
    where
        uuid=?

Hibernate: 
    delete 
    from
        board_image 
    where
        uuid=?


//  게시물 삭제
Hibernate: 
    delete 
    from
        board 
    where
        bno=?

'N+1' 문제와 @BatchSize

  • 상위 엔티티에서 @OneToMany와 같은 연관 관계를 유지하는 경우 한번에 게시물과 첨부파일을 같이 처리할 수 있다는 장점도 있긴 하지만 목록을 처리할 때는 예상치 못한 문제를 만들기 때문 주의
테스트를 위한 더미 데이터 추가
728x90
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/07   »
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
글 보관함