조인과 즉시 로딩, 지연 로딩에서 다음의 코드를 실행하면 오류가 발생한다고 했었다.
String query = "select m from Member m join fetch m.team t on m.team.name = 'A'";
List<Member> resultList = em.createQuery(query, Member.class)
.getResultList();
이에 대해 자세히 알아보자.
일단 On은 조인과 관련된 필터링이라는 것을 알아두고 다음을 보자.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@Column(name = "AGE")
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
@Column(name = "NAME")
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
위와 같이 엔티티를 세팅하고
Team teamA = new Team("A");
Team teamB = new Team("B");
teamRepository.saveAll(Arrays.asList(teamA, teamB));
Member member1 = new Member("member1", 20, teamA);
Member member2 = new Member("member2", 30, teamB);
Member member3 = new Member("member3", 30, teamA);
memberRepository.saveAll(Arrays.asList(member1, member2, member3));
em.flush();
em.clear();
String query
= "select t from Team t join fetch t.members m on m.username = 'member1'";
List<Team> result = em.createQuery(query, Team.class).getResultList();
위와 같이 테스트 코드를 작성했을 때 만약 성공적으로 실행되었다고 가정하자.
원래는 teamA의 members에는 member1과 member3가 있다.
그런데 위의 테스트코드가 성공을 했다면 teamA의 members에는 member1만 들어있게 된다.
페치 조인은 연관된 것을 다 가져오기 위해서 사용하는 것이다.
위의 상황은 이 전제와 맞지 않다!
페치 조인은 연관된 것을 다 가져오기 위해 사용하는건데 On
절을 사용하여 필터링을 한다?
그러면 위의 상황처럼 연관된 것을 다 가져오는 것이 아닌 일부만 가져오게 된다.
따라서 페치 조인과 On을 같이 사용하면 오류가 난다.
그런데 문득 의문이 들 수 있다.
select t from Team t join fetch t.members m on t.name = 'A';
위의 쿼리 같은 경우는 연관된 것을 다 가져오는 것 같은데...?
위의 쿼리를 실행하면 teamA의 members에는 member1과 member3가 모두 있을거 같은데...?
하지만 실행해보면 여지없이 오류가 난다.
이에 대해서는 영한님의 답변이 있다.
적어주신 쿼리의 의도는 사실 다음과 같은 쿼리입니다. 조인과는 무관하게 Team 자체의 데이터를 필터링 하는 것이기 때문에 on에 사용하는 것은 의도에 맞지 않습니다. 따라서 다음과 같이 where를 사용하는 것이 맞습니다.
select t from Team t join fetch t.member m where t.name = 'A';
결론은 fetch 조인 사용시 on절은 사용하지 말라는 것이다.
이제 fetch 조인 사용시 on절을 사용하면 오류가 나는 이유에 대해서 알았다.
그리고 where절을 사용하면 오류가 나지 않음을 또한 알았다.
그런데... where절을 사용할 때도 주의를 해야한다!
Team teamA = new Team("A");
Team teamB = new Team("B");
teamRepository.saveAll(Arrays.asList(teamA, teamB));
Member member1 = new Member("member1", 20, teamA);
Member member2 = new Member("member2", 30, teamB);
Member member3 = new Member("member3", 30, teamA);
memberRepository.saveAll(Arrays.asList(member1, member2, member3));
em.flush();
em.clear();
String query
= "select t from Team t join fetch t.members m where m.username = 'member1'";
List<Team> result = em.createQuery(query, Team.class).getResultList();
for (Team team1 : result) {
System.out.println("team1 = " + team1.getName());
List<Member> members = team1.getMembers();
for (Member member : members) {
System.out.println("member = " + member.getUsername());
}
}
위의 코드를 실행하면 실행결과는 다음과 같이 나온다.
team1 = teamA
member = member1
실행결과를 보면 members에 member가 하나만 존재한다.
어플리케이션에서 fetch join의 결과는 연관된 모든 엔티티가 있을 것이라 가정하고 사용해야 한다. 그런데 이렇게 페치 조인에 별칭을 잘못 사용해서 컬렉션 결과를 필터링 해버리면, 객체의 상태와 DB의 상태의 일관성이 깨지게 되는 것이다.
JPA 표준 스펙에서는 fetch join 대상에 별칭이 없다.
그러나 하이버네이트는 허용한다.
뭔가 위의 상황처럼 별칭을 쓰면 문제가 생길 것 같은데 하이버네이트는 허용을 했다.
이유가 있지 않겠는가! 다음과 같은 상황이라면 별칭을 사용해도 상관없다고 한다.
select m from Member m join fetch m.team t where t.name = 'A';
이 쿼리는 회원과 팀의 일관성을 해치지 않는다.
즉, 조회된 회원은 db와 동일한 일관성을 유지한 팀의 결과를 가지고 있다.
이런 경우에는 사용해도 된다고 한다.
또한 일관성이 깨져도 딱! 조회용으로만 주의해서 사용하면 크게 문제는 없다고 한다.
하지만 이 경우에는 정말 조심해서 조회 용도로만 사용해야 한다.
영한님의 경우도 실무에서 일관성을 해치지 않는 범위에서 성능 최적화를 위해 패치 조인 대상에 별칭을 종종 사용한다고 한다~