Member
public class Member { @Id @GeneratedValue @Column(name = "member_id") private Long id; private String username; private int age; @ManyToOne(fetch = LAZY) @JoinColumn(name = "team_id") private Team team; }
Team
public class Team { @Id @GeneratedValue @Column(name = "team_id") private Long id; private String name; @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<>(); }
Memebr와 Team 객체가 N:1 관계에 있다.
이때 Member 객체를 조회한 후 연관관계에 있는 Team 객체를 사용한다면?
@Test public void findMemberLazy() { //given //member1 -> teamA //member2 -> teamB Team teamA = new Team("teamA"); Team teamB = new Team("teamB"); teamRepository.save(teamA); teamRepository.save(teamB); Member member1 = new Member("member1", 10, teamA); Member member2 = new Member("member2", 20, teamB); memberRepository.save(member1); memberRepository.save(member2); em.flush();; em.clear(); //when List<Member> members = memberRepository.findAll(); for (Member member : members) { System.out.println("member = " + member); //member.teamClass = class study.datajpa.Entity.Team$HibernateProxy$wL9kzFrK System.out.println("member.teamClass = " + member.getTeam().getClass()); System.out.println("member.team = " + member.getTeam().getName()); } }
FetchType이 Lazy이므로 JPA는 Team을 프록시 객체로 가져오게 된다.
이후에 Team 객체를 사용할 때 마다 DB로 조회 쿼리를 보내게 된다.
이것이 JPA에서 자주 발생하는 N + 1 문제이다.
쿼리 코드
// fetch를 하게 되면 join 후 select절에 team 객체 데이터도 포함해준다. @Query("select m from Member m left join fetch m.team") List<Member> findMemberFetchJoin();
JPA가 보낸 쿼리
select member0_.member_id as member_i1_0_0_, team1_.team_id as team_id1_1_1_, member0_.age as age2_0_0_, member0_.team_id as team_id4_0_0_, member0_.username as username3_0_0_, team1_.name as name2_1_1_ from member member0_ left outer join team team1_ on member0_.team_id=team1_.team_id;
테스트 코드
@Test public void findMemberLazy() { //given //member1 -> teamA //member2 -> teamB Team teamA = new Team("teamA"); Team teamB = new Team("teamB"); teamRepository.save(teamA); teamRepository.save(teamB); Member member1 = new Member("member1", 10, teamA); Member member2 = new Member("member2", 20, teamB); memberRepository.save(member1); memberRepository.save(member2); em.flush();; em.clear(); //when 쿼리 수정 List<Member> members = memberRepository.findMemberFetchJoin(); for (Member member : members) { System.out.println("member = " + member); //member.teamClass = class study.datajpa.Entity.Team System.out.println("member.teamClass = " + member.getTeam().getClass()); System.out.println("member.team = " + member.getTeam().getName()); } }
이렇게 해주면 Member와 연관관계에 있는 Team 객체를 fetch join 하여 실제 Team 객체를 생성하게 된다. 실제 Team 객체사 생성되었으므로 Team 객체를 사용할 때 마다 조회 쿼리를 보내지 않는다. JPA에선 이런식으로 N + 1 문제를 해결한다.
// Data JPA가 제공하는 findAll을 override 해서 사용할 경우 @Override @EntityGraph(attributePaths = {"team"}) List<Member> findAll(); // JPQL로 직접 작성할 경우 @EntityGraph(attributePaths = {"team"}) @Query("select m from Member m") List<Member> findMemberEntityGraph(); // Named 쿼리를 사용할 경우 @EntityGraph(attributePaths = {"team"}) List<Member> findEntityGraphByUsername(@Param("username") String username);
위 3가지 경우 모두 @EntityGraph를 사용할 수 있다. attributePaths 안에 명시한 연관관계 객체를 fetch join 해서 가져오게 된다.