티스토리 뷰
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
링크
TAG
- reactApp
- 서버전송
- 제약조건
- 다른데서 react
- ID
- 셀렉트박스
- selectbox
- commit 에러
- 아이디
- 씹어먹는 C 언어
- 받아오기
- 체크박스
- SCP
- CheckBox
- C
- JPA
- mircrosoft visual studio
- @Builder
- reactStart
- 다중체크박스 처리
- 셀프로젝트
- @RequestParam
- findFirstBy
- C언어
- th:selected
- @reqeustBody
- react 시작 오류
- App
- React
- optional
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함