먼저 주의점의 관찰을 위해 프로젝트를 세팅한다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String loginId;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String nickName;
@Builder
public Member(String name,String loginId,String password,String nickName,Team team){
this.name=name;
this.loginId=loginId;
this.password=password;
this.nickName=nickName;
this.team=team;
}
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
}
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "team",cascade = CascadeType.ALL,orphanRemoval = true)
private List<Member> members=new ArrayList<>();
@OneToMany(mappedBy = "team",cascade = CascadeType.ALL,orphanRemoval = true)
private List<TeamImage> teamImages=new ArrayList<>();
@Builder
public Team(String name){
this.name=name;
}
}
Team 과 TeamImage 의 관계를 세팅한다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class TeamImage {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String accessUrl;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
@Builder
public TeamImage(String accessUrl,Team team){
this.accessUrl=accessUrl;
this.team=team;
}
}
실험 환경 기준은 하이버네이트 버전 5.6.14
버전이다.
@ManyToOne
에서 다(Member) 쪽에서 일(Team)쪽을 fetch Join 을 사용하여 일(Team) 의 필드 또한 함께 영속화 할 수 있다는 것을 알게된 A는
이제는 일(Team) 을 조회할 때 그 와 연관된 다(Member) 의 필드를 함께 조회하고 싶어 일(Team) 에서 다(Member)에 Fetch Join 을 적용했다.
아래와 같이 말이다.
public interface TeamRepository extends JpaRepository<Team,Long> {
@Query("select t from Team t join fetch t.members")
List<Team> findTeamWithFetchMembers();
}
A는 처음에 위 코드를 짤때 원하는 결과는 각각의 팀을 한번씩 조회하고 그와 연관된 Member 또한 같이 조회하는 결과를 원했다.
즉 아래와 같이 테스트 코드를 통해 결과를 확인하면
@Test
@DisplayName("팀과 Member 의 1대다 Fetch Join 테스트 Pageable")
public void SecondTest() {
Team teamA=Team.builder().name("teamA").build();
Team teamB=Team.builder().name("teamB").build();
Member member1=Member.builder().loginId("afdf").password("adfdf").nickName("일진우").name("이진우1").team(teamA).build();
Member member2=Member.builder().loginId("bfdf").password("bdfdf").nickName("이진우").name("이진우2").team(teamA).build();
Member member3=Member.builder().loginId("cfdf").password("cdfdf").nickName("삼진우").name("이진우3").team(teamA).build();
Member member4=Member.builder().loginId("dfdf").password("ddfdf").nickName("사진우").name("이진우4").team(teamB).build();
Member member5=Member.builder().loginId("efdf").password("edfdf").nickName("오진우").name("이진우5").team(teamB).build();
teamA.getMembers().add(member1);
teamA.getMembers().add(member2);
teamA.getMembers().add(member3);
teamB.getMembers().add(member4);
teamB.getMembers().add(member5);
teamRepository.save(teamA);
teamRepository.save(teamB);
em.clear();
List<Team> teams=teamRepository.findTeamWithFetchMembersWithPageable(PageRequest.of(0,2));
for(Team t: teams){
System.out.println("팀의 이름: "+t.getName());
System.out.println(t.getMembers().size());
}
}
즉 예상된 출력결과는 팀의 이름 A: 와 팀의 Member 에 대한 각각의 이름 + 팀의 이름 :B 와 팀 B의 Member 에 대한 각각의 이름
이렇게만 나오는 것을 예상했지만 출력결과는 그렇지 않다.
팀의 이름: teamA
멤버의 이름:이진우1
멤버의 이름:이진우2
멤버의 이름:이진우3
팀의 이름: teamA
멤버의 이름:이진우1
멤버의 이름:이진우2
멤버의 이름:이진우3
팀의 이름: teamA
멤버의 이름:이진우1
멤버의 이름:이진우2
멤버의 이름:이진우3
팀의 이름: teamB
멤버의 이름:이진우4
멤버의 이름:이진우5
팀의 이름: teamB
멤버의 이름:이진우4
멤버의 이름:이진우5
그 이유는 쿼리 내용을 확인하면 짐작이 가능하다.
select
team0_.id as id1_1_0_,
members1_.member_id as member_i1_0_1_,
team0_.name as name2_1_0_,
members1_.created_at as created_2_0_1_,
members1_.updated_at as updated_3_0_1_,
members1_.login_id as login_id4_0_1_,
members1_.name as name5_0_1_,
members1_.nick_name as nick_nam6_0_1_,
members1_.password as password7_0_1_,
members1_.team_id as team_id8_0_1_,
members1_.team_id as team_id8_0_0__,
members1_.member_id as member_i1_0_0__
from
team team0_
inner join
member members1_
on team0_.id=members1_.team_id
team의 아이디와 member의 team_id가 같은데에 join이 일어난다.
그럼 위 같은 쿼리의 결과로 나타나는 테이블의 형태는
팀A 와 멤버 1에 대한 레코드 +
팀A 와 멤버 2에 대한 레코드 +
팀A 와 멤버 3에 대한 레코드 +
팀B 와 멤버 4에 대한 레코드 +
팀 B와 멤버 5에 대한 레코드
이런 식으로 나타날 것이다.
따라서 위와 같이 문제의 출력결과가 발생한다.
이를 방지하려면 distinct
키워드를 통해 방지할 수 있다.
이는 객체의 중복을 방지하게 한다.
public interface TeamRepository extends JpaRepository<Team,Long> {
@Query("select distinct t from Team t join fetch t.members")
List<Team> findTeamWithFetchMembers();
}
팀의 이름: teamA
멤버의 이름:이진우1
멤버의 이름:이진우2
멤버의 이름:이진우3
팀의 이름: teamB
멤버의 이름:이진우4
멤버의 이름:이진우5
위와 같이 Team 이 중복되어 조회되지 않는 것을 볼 수 있다.
HiberName 버젼의 상승과 함께 초기처럼 코드를 작성해도 자동으로 중복은 제거해서 아래와 같은 결과가 나타난다.
팀의 이름: teamA
멤버의 이름:이진우1
멤버의 이름:이진우2
멤버의 이름:이진우3
팀의 이름: teamB
멤버의 이름:이진우4
멤버의 이름:이진우5
A는 주의점 1 과 같은 결과를 학습했으므로
이제 페이징 처리를 일대다 페치조인에 적용하여 제한된 양을 조회하고 싶다고 한다.
따라서 아래와 같이 코드를 작성하였다.
public interface TeamRepository extends JpaRepository<Team,Long> {
@Query("select t from Team t join fetch t.members")
List<Team> findTeamWithFetchMembersWithPageable(Pageable pageable);
}
따라서 아래와 같이 테스트를 진행하여 결과를 확인한다.
@Test
@DisplayName("팀과 Member 의 1대다 Fetch Join 테스트 Pageable")
public void SecondTest() {
Team teamA=Team.builder().name("teamA").build();
Team teamB=Team.builder().name("teamB").build();
Member member1=Member.builder().loginId("afdf").password("adfdf").nickName("일진우").name("이진우1").team(teamA).build();
Member member2=Member.builder().loginId("bfdf").password("bdfdf").nickName("이진우").name("이진우2").team(teamA).build();
Member member3=Member.builder().loginId("cfdf").password("cdfdf").nickName("삼진우").name("이진우3").team(teamA).build();
Member member4=Member.builder().loginId("dfdf").password("ddfdf").nickName("사진우").name("이진우4").team(teamB).build();
Member member5=Member.builder().loginId("efdf").password("edfdf").nickName("오진우").name("이진우5").team(teamB).build();
teamA.getMembers().add(member1);
teamA.getMembers().add(member2);
teamA.getMembers().add(member3);
teamB.getMembers().add(member4);
teamB.getMembers().add(member5);
teamRepository.save(teamA);
teamRepository.save(teamB);
em.clear();
List<Team> teams=teamRepository.findTeamWithFetchMembersWithPageable(PageRequest.of(0,2));
for(Team t: teams){
System.out.println("팀의 이름: "+t.getName());
for(int i=0;i<t.getMembers().size();i++){
System.out.println("멤버의 이름:"+t.getMembers().get(i).getName());
}
}
}
결과를 확인하던 중 아래와 같은 에러가 발생한다.
2024-03-16T15:10:47.651+09:00 WARN 14408 --- [ Test worker] org.hibernate.orm.query : HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory
이는 메모리에서 페이징 처리를 진행한다는 의미로
기존처럼 DB에서 limit
로 제한된 갯수만큼을 가져오는 것이 아니라 메모리에서 그 작업을 진행하기 때문에 그런 부분에서 위험하다.
실제로 일대다 페치조인+ 페이징 처리에서 발생하는 쿼리를 조회하면
select
t1_0.id,
m1_0.team_id,
m1_0.member_id,
m1_0.login_id,
m1_0.name,
m1_0.nick_name,
m1_0.password,
t1_0.name
from
team t1_0
join
member m1_0
on t1_0.id=m1_0.team_id
위와 같이 limit
이 보이지 않는다는 것을 알 수 있다.
반면 다대일 페치조인 + 페이징 처리를 진행하면
List<Member> members=memberRepository.findMemberWithTeamFetch(PageRequest.of(0,2));
select
m1_0.member_id,
m1_0.login_id,
m1_0.name,
m1_0.nick_name,
m1_0.password,
t1_0.id,
t1_0.name
from
member m1_0
join
team t1_0
on t1_0.id=m1_0.team_id
limit
?,?
위와 같이 limit
이 존재한다.
A는 Fetch Join
을 사용해서 Member 의 데이터의 일부와 Team 의 데이터 일부를 함께 DTO로 반환한다.
@Data
@NoArgsConstructor
public class MemberResponseDto {
private String memberNickName;
private String teamName;
@Builder
public MemberResponseDto(String memberNickName,String teamName){
this.memberNickName=memberNickName;
this.teamName=teamName;
}
}
또한 위 DTO 를 설계하기 위해
@Query("select m from Member m join fetch m.team t")
List<Member> findMemberWithTeamFetch(Pageable pageable);
위 메서드를 사용하고 있는데 기본적으로 Member 의 내용 전부를 조회하다 보니 아래와 같이
select
m1_0.member_id,
m1_0.login_id,
m1_0.name,
m1_0.nick_name,
m1_0.password,
t1_0.id,
t1_0.name
from
member m1_0
join
team t1_0
on t1_0.id=m1_0.team_id
limit
?,?
실제 사용하지 않는 password
, id
등 필요없는 부분을 함께 조회하고 있다.
위 과정에서 성능이 걱정되었던 A 는 이를 DTO로 조회하여 해결하려 한다.
따라서 아래와 같이 코드를 작성한다.
@Query("select new com.ceos19.everyTime.member.domain.dto.MemberResponseDto(m.nickName,t.name) from Member m join fetch m.team t")
List<MemberResponseDto> findMemberDTOWithTeamNameWithFetchJoin();
위에 대해 테스트를 작성하기 위해 아래와 같은 코드를 작성한다.
@Test
@DisplayName("Member 입장에서 테스트")
public void MemberTest() {
Team teamA=Team.builder().name("teamA").build();
Team teamB=Team.builder().name("teamB").build();
Member member1=Member.builder().loginId("afdf").password("adfdf").nickName("일진우").name("이진우1").team(teamA).build();
Member member2=Member.builder().loginId("bfdf").password("bdfdf").nickName("이진우").name("이진우2").team(teamA).build();
Member member3=Member.builder().loginId("cfdf").password("cdfdf").nickName("삼진우").name("이진우3").team(teamA).build();
Member member4=Member.builder().loginId("dfdf").password("ddfdf").nickName("사진우").name("이진우4").team(teamB).build();
Member member5=Member.builder().loginId("efdf").password("edfdf").nickName("오진우").name("이진우5").team(teamB).build();
teamA.getMembers().add(member1);
teamA.getMembers().add(member2);
teamA.getMembers().add(member3);
teamB.getMembers().add(member4);
teamB.getMembers().add(member5);
teamRepository.save(teamA);
teamRepository.save(teamB);
em.clear();
List<MemberResponseDto> memberResponseDtos=memberRepository.findMemberDTOWithTeamNameWithFetchJoin();
for(MemberResponseDto memberResponseDto : memberResponseDtos){
System.out.println("팀의 이름: "+memberResponseDto.getTeamName());
System.out.println("멤버의 이름: "+memberResponseDto.getMemberNickName());
}
}
하지만 아래와 같은 에러가 발생한다.
Caused by: org.hibernate.query.SemanticException:
query specified join fetching, but the owner of the fetched
association was not present in the select list [SqmSingularJoin
(com.ceos19.everyTime.member.domain.Member(m).team(t) : team)]
페치 연관관계가 select list에 없다고 한다.
fetch join
을 없애고 join
으로만 사용한다.
select
m1_0.nick_name,
t1_0.name
from
member m1_0
join
team t1_0
on t1_0.id=m1_0.team_id
그럼 위와 같은 쿼리로 최적화를 진행할 수 있다.
A는 Team 에서 Team 의 이미지와 Team 의 Member 에 대한 내용 을 함께 영속화해서 가져오고 싶다.
따라서 아래와 같은 코드를 작성한다.
@Query("select t from Team t join fetch t.members join fetch t.teamImages")
List<Team> findTeamWithFetchMembersAndImages();
@Test
@DisplayName("팀과 Member 의 1대다 Fetch Join 테스트 Pageablefadfa")
public void ThirdTest() {
Team teamA=Team.builder().name("teamA").build();
Team teamB=Team.builder().name("teamB").build();
Member member1=Member.builder().loginId("afdf").password("adfdf").nickName("일진우").name("이진우1").team(teamA).build();
Member member2=Member.builder().loginId("bfdf").password("bdfdf").nickName("이진우").name("이진우2").team(teamA).build();
Member member3=Member.builder().loginId("cfdf").password("cdfdf").nickName("삼진우").name("이진우3").team(teamA).build();
Member member4=Member.builder().loginId("dfdf").password("ddfdf").nickName("사진우").name("이진우4").team(teamB).build();
Member member5=Member.builder().loginId("efdf").password("edfdf").nickName("오진우").name("이진우5").team(teamB).build();
TeamImage teamImage=TeamImage.builder().team(teamA).accessUrl("www").build();
TeamImage teamImage1=TeamImage.builder().team(teamA).accessUrl("www").build();
teamA.getMembers().add(member1);
teamA.getMembers().add(member2);
teamA.getMembers().add(member3);
teamB.getMembers().add(member4);
teamB.getMembers().add(member5);
teamA.getTeamImages().add(teamImage);
teamA.getTeamImages().add(teamImage1);
teamRepository.save(teamA);
teamRepository.save(teamB);
em.clear();
List<Team> teams=teamRepository.findTeamWithFetchMembersAndImages();
for(Team t: teams){
System.out.println("팀의 이름: "+t.getName());
System.out.println(t.getMembers().size());
}
}
위와 같은 테스트 코드를 작성하고 돌리면
그럼 아래와 같은
Caused by: java.lang.IllegalArgumentException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.ceos19.everyTime.member.domain.Team.members, com.ceos19.everyTime.member.domain.Team.teamImages]
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:141)
at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:378)
at org.hibernate.query.Query.getResultList(Query.java:119)
여러 객체에 대해 페치조인을 사용하지 말라는 오류를 발생시킨다.