Board와 Reply는 1:N 관계이며, 즉시 로딩으로 설정하였다.
@Entity
@Table(name = "board_tb")
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@OneToMany(mappedBy = "board", fetch = FetchType.EAGER)
private List<Reply> replyList = new ArrayList<>();
@Column(nullable = false)
private String title;
@Lob
@Column(nullable = false)
private String content;
}
@Table(name="reply_tb")
public class Reply {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@ManyToOne(fetch = FetchType.LAZY)
private User user;
@ManyToOne
private Board board;
@Column(nullable = false)
private String comment;
}
em.find()
Hibernate:
select
board0_.id as id1_0_0_,
board0_.content as content2_0_0_,
board0_.title as title3_0_0_,
replylist1_.board_id as board_id3_1_1_,
replylist1_.id as id1_1_1_,
replylist1_.id as id1_1_2_,
replylist1_.board_id as board_id3_1_2_,
replylist1_.comment as comment2_1_2_,
replylist1_.user_id as user_id4_1_2_
from
board_tb board0_
left outer join
reply_tb replylist1_
on board0_.id=replylist1_.board_id
where
board0_.id=?
위 코드를 해석을 해보자면, 주어진 board_id에 해당하는 게시물(board_tb)과 이에 연결된 댓글(reply_tb)의 정보를 검색하여 반환한다는 뜻이며, 자세한 설명은 아래 적어두었다.
외부 조인 → 조인문의 왼쪽에 있는 테이블의 모든 결과를 가져오고, 오른쪽 테이블은 조건에 맞는 결과만 가져온다.
쿼리에 대한 자세한 설명
select
: 쿼리 결과로 반환될 열(칼럼)들을 선택합니다.
board0_.id as id1_0_0_
: 게시물의 ID (board_tb의 id 열)board0_.content as content2_0_0_
: 게시물의 내용 (board_tb의 content 열)board0_.title as title3_0_0_
: 게시물의 제목 (board_tb의 title 열)replylist1_.board_id as board_id3_1_1_
: 댓글의 게시물 ID (reply_tb의 board_id 열)replylist1_.id as id1_1_1_
: 댓글의 ID (reply_tb의 id 열)replylist1_.id as id1_1_2_
: 댓글의 ID (reply_tb의 id 열)replylist1_.board_id as board_id3_1_2_
: 댓글의 게시물 ID (reply_tb의 board_id 열)replylist1_.comment as comment2_1_2_
: 댓글의 내용 (reply_tb의 comment 열)replylist1_.user_id as user_id4_1_2_
: 댓글의 사용자 ID (reply_tb의 user_id 열)from
: 게시물 테이블(board_tb)을 선택합니다.
board0_
는 게시물 테이블(board_tb)에 대한 별칭(alias)입니다.left outer join
: 왼쪽 외부 조인을 수행합니다. 게시물 테이블(board_tb)과 댓글 테이블(reply_tb)을 board_tb의 id와 reply_tb의 board_id 열을 기준으로 조인합니다.
on board0_.id=replylist1_.board_id
: board_tb의 id 열과 reply_tb의 board_id 열을 사용하여 조인합니다.where
: 조건을 설정합니다.
- board0_.id=?
: board_tb의 id 값이 주어진 값과 일치하는 게시물을 선택합니다.
위의 방식에서는 SQL을 두 번 실행, 즉 두번의 Select를 통해서 Board와 Reply를 가져오는 것이 아니라 조인을 사용해서 한 번의 SQL로 Board와 Reply를 함께 조회한다.
// when
em.createQuery("select b from Board b", Board.class)
.getResultList();
Hibernate:
select
board0_.id as id1_0_,
board0_.content as content2_0_,
board0_.title as title3_0_
from
board_tb board0_
Hibernate:
select
replylist0_.board_id as board_id3_1_1_,
replylist0_.id as id1_1_1_,
replylist0_.id as id1_1_0_,
replylist0_.board_id as board_id3_1_0_,
replylist0_.comment as comment2_1_0_,
replylist0_.user_id as user_id4_1_0_
from
reply_tb replylist0_
where
replylist0_.board_id=?
@BeforeEach
public void set_up() {
User user = userRepository.save(newUser("kevin"));
Board board = boardRepository.save(newBoard("반갑꼬리!~", user));
replyRepository.save(newReply("나두 반갑꼬리~!1", user, board));
replyRepository.save(newReply("나두 반갑꼬리~!2", user, board));
replyRepository.save(newReply("나두 반갑꼬리~!3", user, board));
replyRepository.save(newReply("나두 반갑꼬리~!4", user, board));
replyRepository.save(newReply("나두 반갑꼬리~!5", user, board));
em.flush();
em.clear();
}
Hibernate:
select
replylist0_.board_id as board_id3_1_1_,
replylist0_.id as id1_1_1_,
replylist0_.id as id1_1_0_,
replylist0_.board_id as board_id3_1_0_,
replylist0_.comment as comment2_1_0_,
replylist0_.user_id as user_id4_1_0_
from
reply_tb replylist0_
where
replylist0_.board_id=?
Hibernate:
select
replylist1_.board_id as board_id3_1_1_,
replylist1_.id as id1_1_1_,
replylist1_.id as id1_1_0_,
replylist1_.board_id as board_id3_1_0_,
replylist1_.comment as comment2_1_0_,
replylist1_.user_id as user_id4_1_0_
from
reply_tb replylist1_
where
replylist1_.board_id=?
Hibernate:
select
replylist2_.board_id as board_id3_1_1_,
replylist2_.id as id1_1_1_,
replylist2_.id as id1_1_0_,
replylist2_.board_id as board_id3_1_0_,
replylist2_.comment as comment2_1_0_,
replylist2_.user_id as user_id4_1_0_
from
reply_tb replylist2_
where
replylist2_.board_id=?
...
default_batch_fetch_size: 100
가 설정되어있었기에 강제로 sql의 in절을 사용하게 된 것이다. ㅋㅋㅋㅋㅋㅋㅋㅋHibernate:
select
replylist0_.board_id as board_id1_1_1_,
replylist0_.reply_list_id as reply_li2_1_1_,
reply1_.id as id1_2_0_,
reply1_.board_id as board_id3_2_0_,
reply1_.comment as comment2_2_0_
from
board_tb_reply_list replylist0_
inner join
reply_tb reply1_
on replylist0_.reply_list_id=reply1_.id
where
replylist0_.board_id in (
?, ?
)
@Lob은 일반적인 데이터베이스에서 저장하는 길이인 255개 이상의 문자를 저장하고 싶을 때 지정한다.
그러면 지연 로딩을 설정하면 어떻게 될까?
Hibernate:
select
board0_.id as id1_0_,
board0_.content as content2_0_,
board0_.title as title3_0_
from
board_tb board0_
Hibernate:
select
replylist0_.board_id as board_id1_1_0_,
replylist0_.reply_list_id as reply_li2_1_0_,
reply1_.id as id1_2_1_,
reply1_.board_id as board_id3_2_1_,
reply1_.comment as comment2_2_1_
from
board_tb_reply_list replylist0_
inner join
reply_tb reply1_
on replylist0_.reply_list_id=reply1_.id
where
replylist0_.board_id=?
List<Board> boards = em.createQuery("select b from Board b", Board.class)
.getResultList();
System.out.println("============== N+1 시점 확인용 ===================");
for (Board board : boards) {
System.out.println("board = " + board.getReplyList().size());
Hibernate:
select
replylist0_.board_id as board_id1_1_0_,
replylist0_.reply_list_id as reply_li2_1_0_,
reply1_.id as id1_2_1_,
reply1_.board_id as board_id3_2_1_,
reply1_.comment as comment2_2_1_
from
board_tb_reply_list replylist0_
inner join
reply_tb reply1_
on replylist0_.reply_list_id=reply1_.id
where
replylist0_.board_id=?
Hibernate:
select
replylist0_.board_id as board_id1_1_0_,
replylist0_.reply_list_id as reply_li2_1_0_,
reply1_.id as id1_2_1_,
reply1_.board_id as board_id3_2_1_,
reply1_.comment as comment2_2_1_
from
board_tb_reply_list replylist0_
inner join
reply_tb reply1_
on replylist0_.reply_list_id=reply1_.id
where
replylist0_.board_id=?
fetch join은 SQL 조인을 사용해서 연관된 엔티티를 함께 조회하므로, N+1 문제가 발생하지 않는다.
List<Board> boards = em.createQuery("select b from Board b join fetch b.replyList", Board.class)
.getResultList();
System.out.println("============== N+1 시점 확인용 ===================");
// then
for (Board board : boards) {
System.out.println("board = " + board.getReplyList().size());
Hibernate:
select
board0_.id as id1_0_0_,
reply2_.id as id1_2_1_,
board0_.content as content2_0_0_,
board0_.title as title3_0_0_,
reply2_.board_id as board_id3_2_1_,
reply2_.comment as comment2_2_1_,
replylist1_.board_id as board_id1_1_0__,
replylist1_.reply_list_id as reply_li2_1_0__
from
board_tb board0_
inner join
board_tb_reply_list replylist1_
on board0_.id=replylist1_.board_id
inner join
reply_tb reply2_
on replylist1_.reply_list_id=reply2_.id
단 1:N 조인의 경우 결과가 늘어나서, 중복된 결과가 나타날 수 있으니 JPQL의 DISTNICT를 사용해서 중복을 제거하는게 좋다.
default_batch_fetch_size: N
을 설정하는 방법이다. 해당 설정을 통해서 연관된 엔티티를 조회할 때 batch size로 설정해둔 만큼 SQL의 IN절을 사용해서 조회한다.특정 필드 위만 @org.hibernate.annotations.BatchSize(size = N)로 설정할 수 있다.
Hibernate:
select
replylist0_.board_id as board_id1_1_1_,
replylist0_.reply_list_id as reply_li2_1_1_,
reply1_.id as id1_2_0_,
reply1_.board_id as board_id3_2_0_,
reply1_.comment as comment2_2_0_
from
board_tb_reply_list replylist0_
inner join
reply_tb reply1_
on replylist0_.reply_list_id=reply1_.id
where
replylist0_.board_id in (
?, ?
)
@OneToOne
과 @ManyToOne
은 지연 로딩으로 설정하고, 성능 최적화가 필요한 곳에서는 JPQL 페치 조인을 사용하자.