JPA의 fetchjoin 기술(지연로딩개념 : LAZY)을 명확히 이해한 다음에
@EntityGraph로 넘어오자.
먼저 MemberRepositoryTest를 작성하면 member1은 teamA를 참조하고
member2는 teamB를 참조한다.
@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("member1", 10, teamB);
memberRepository.save(member1);
memberRepository.save(member2);
// 영속성 컨텍스트의 있는 캐시 정보들을 db에 완전히 반영을 해서 INSERT를 정확하게 다하고 db에 다 반영을 시킨 다음에
em.flush();
// clear를 다한 후에 영속성 컨텍스트를 다 날려버리는 것이다.
em.clear();
}
Member와 team의 관계는 ManyToOne이고 fetch = LAZY로 설정해놓았다.
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "tema_id")
private Team team;
MemberRepositoryTest를 실행시키면
@Test
public void bulkUpdate() {
//given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 19));
memberRepository.save(new Member("member3", 20));
memberRepository.save(new Member("member4", 21));
memberRepository.save(new Member("member5", 40));
// when
int resultCount = memberRepository.bulkAgePlus(20);
// em.clear();
List<Member> result = memberRepository.findByUsername("member5");
Member member5 = result.get(0);
System.out.println("member5 = " + member5);
// then
assertThat(resultCount).isEqualTo(3);
}
@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", 10, teamB);
memberRepository.save(member1);
memberRepository.save(member2);
em.flush();
em.clear();
// 멤버객체만 쭉 뽑아오기 위해
List<Member> members = memberRepository.findAll();
for (Member member : members) {
System.out.println("member = " + member.getUsername());
}
}
한번의 쿼리로 결과 두개를 가져왔다.
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.tema_id as tema_id4_0_,
member0_.username as username3_0_
from
member member0_
member = member1
member = member2
그리고 getTeam().getClass를 해서 team의 class를 찍어보면 순서가
member query가 나가고 member1 찍고
teamClass를 찍은 다음에 team id를 가지고 query를 날려서 데이터를 가지고 온다.
그래서 teamA가 찍혀진다.
@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", 10, teamB);
memberRepository.save(member1);
memberRepository.save(member2);
em.flush();
em.clear();
// when
// 멤버객체만 쭉 뽑아오기 위해
// select Member!
List<Member> members = memberRepository.findAll();
for (Member member : members) {
System.out.println("member = " + member.getUsername());
System.out.println("member.teamClass = " + member.getTeam().getClass());
System.out.println("member.team = " + member.getTeam().getName());
}
}
그리고 두번째 루프에서 member2가 찍혀지고 proxy찍고
member2와 연관된 teamB가 어플리케이션 로딩과 연관이 안되어있어서 db쿼리를 날린다. 그래서 team에 대한 쿼리가 두번 날라가게 된다. 그리고 teamB가 조회된다.
N + 1 문제라고 볼 수 있는데 query를 한번 날렸는데 member에 대한 결과는 2번이 나와서 n + 1 문제라고 할 수 있다.
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.tema_id as tema_id4_0_,
member0_.username as username3_0_
from
member member0_
member = member1
member.teamClass = class study.datajpa.entity.Team$HibernateProxy$Zsh4bA5f
select
team0_.team_id as team_id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.team_id=?
member.team = teamA
member = member2
member.teamClass = class study.datajpa.entity.Team$HibernateProxy$Zsh4bA5f
select
team0_.team_id as team_id1_1_0_,
team0_.name as name2_1_0_
from
team team0_
where
team0_.team_id=?
member.team = teamB
JPA에서는 이걸 fetchjoin이라는 걸로 해결을 해주는데 Member를 조회할 때 연관된 team을 같이 한번에 다 끌고온다.
// MemberRepository
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
테스트를 돌려보면
// MemberRepositoryTest
@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", 10, teamB);
memberRepository.save(member1);
memberRepository.save(member2);
em.flush();
em.clear();
// when
// 멤버객체만 쭉 뽑아오기 위해
// select Member! N + 1 문제
List<Member> members = memberRepository.findMemberFetchJoin();
for (Member member : members) {
System.out.println("member = " + member.getUsername());
System.out.println("member.teamClass = " + member.getTeam().getClass());
System.out.println("member.team = " + member.getTeam().getName());
}
}
아래 부분에서 정확하게 join을 해서 member에 대한 데이터와 team에 대한 데이터를 다 select한다.
한번에 다 끌고 오기 때문에
List<Member> members = memberRepository.findMemberFetchJoin();
select
member0_.member_id as member_i1_0_0_,
team1_.team_id as team_id1_1_1_,
member0_.age as age2_0_0_,
member0_.tema_id as tema_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_.tema_id=team1_.team_id
team 엔티티까지 같이 생성해서 Team team에다가 넣어놓는다.
// Member class
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
private int age;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "tema_id")
private Team team;
즉 아래 부분에서 proxy라는 것이 붙지않고
System.out.println("member = " + member.getUsername());
System.out.println("member.teamClass = " + member.getTeam().getClass());
class명이 확실하게 붙는다. 즉 순수하게 team 객체가 잘 되었다고 보면 된다.(proxy가 아니구나)
그리고 바로 teamA가 출력이 된다.
member = member1
member.teamClass = class study.datajpa.entity.Team
member.team = teamA
fetch join을 객체그래프로 표현된 (member.team) 연관된 것을 db에 join을 해서 한번에 select 포함시켜서 다 끌고 오는것이다.
db의 join은 join만 하는 것인데 select절에 데이터를 다 넣어준다.
즉 원하는 member를 조회할 때 한방에 쿼리로 끝내는 것이다.
스프링 데이터 JPA에서는 EntityGraph라는 것으로 findByUsername처럼 메서드 이름으로 해결을 해야하는데
fetchjoin까지 같이 하고싶은 거까지 깔끔하게 해결해준다.
// MemberRepository
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
테스트를 돌려주면 lazy 로딩 되는 것이 없고
//
@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", 10, teamB);
memberRepository.save(member1);
memberRepository.save(member2);
em.flush();
em.clear();
// when
// 멤버객체만 쭉 뽑아오기 위해
// select Member! N + 1 문
List<Member> members = memberRepository.findAll();
for (Member member : members) {
System.out.println("member = " + member.getUsername());
System.out.println("member.teamClass = " + member.getTeam().getClass());
System.out.println("member.team = " + member.getTeam().getName());
}
}
member랑 team이랑 다가지고 온다.
select
member0_.member_id as member_i1_0_0_,
team1_.team_id as team_id1_1_1_,
member0_.age as age2_0_0_,
member0_.tema_id as tema_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_.tema_id=team1_.team_id
2022-04-17 22:46:49.540 INFO 40888 --- [ main] p6spy : #1650203209540 | took 0ms | statement | connection 3| url jdbc:h2:tcp://localhost/~/datajpa
select member0_.member_id as member_i1_0_0_, team1_.team_id as team_id1_1_1_, member0_.age as age2_0_0_, member0_.tema_id as tema_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_.tema_id=team1_.team_id
select member0_.member_id as member_i1_0_0_, team1_.team_id as team_id1_1_1_, member0_.age as age2_0_0_, member0_.tema_id as tema_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_.tema_id=team1_.team_id;
member = member1
member.teamClass = class study.datajpa.entity.Team
member.team = teamA
member = member2
member.teamClass = class study.datajpa.entity.Team
member.team = teamB
즉 JPQL없이도 객체그래프를 한번에 엮어서 성능 최적화를 가져오는게 가능하다.
그리고 JPQL도 짜고 FetchJOIN도 가능하다.(JPQL에다가 entity graph를 넣는 것이다.)
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityhGraph();
또 회원데이터를 쓸 때 팀 데이터를 쓸 일이 많이 생길수 있으므로 아래코드와 같이 엔티티 그래프를 뽑을 수 있다.
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
// 엔티티 그래프를 뽑을 수 있다.
@EntityGraph(attributePaths = {"team"})
List<Member> findEntityGraphByUsername(@Param("username") String username);
fetchjoin의 기본형인 left outer join이 나가고 select문이 아래와 같이 나온다. 그리고 데이터를 똑같이 쭉 출력한다.
=> entityGraph를 사용하면 fetchjoin을 굉장히 편리하게 쓸 수 있다.
select
member0_.member_id as member_i1_0_0_,
team1_.team_id as team_id1_1_1_,
member0_.age as age2_0_0_,
member0_.tema_id as tema_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_.tema_id=team1_.team_id
where
member0_.username=?
member = member1
member.teamClass = class study.datajpa.entity.Team
member.team = teamA
member = member1
member.teamClass = class study.datajpa.entity.Team
member.team = teamB
이번에는 member class로 가서 JPA의 표준스펙인 NamedEntityGraph를 쓸 수가 있다.
@NamedQuery(
name="Member.findByUsername",
query="select m from Member m where m.username = :username"
)
@NamedEntityGraph(name = "Member.all", attributeNodes = @NamedAttributeNode("team"))
MemberRepository에서는 아래와 같이 설정해주고 실행시키면 결과가 똑같이 나온다.
// @EntityGraph(attributePaths = {"team"})
@EntityGraph("Member.all")
List<Member> findEntityGraphByUsername(@Param("username") String username);
select
member0_.member_id as member_i1_0_0_,
team1_.team_id as team_id1_1_1_,
member0_.age as age2_0_0_,
member0_.tema_id as tema_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_.tema_id=team1_.team_id
where
member0_.username=?
아래와 같이 두가지 방법이 있는데 왠만하면 attributePaths나 JPQL예서 Fetchjoin을 쓰는 것이 효율적이다.
간단할때는 EntityGraph / 복잡한 쿼리가 반영되면 JPQL Fetchjoin을 쓴다.
@EntityGraph(attributePaths = {"team"})
// @EntityGraph("Member.all")
EntityGraph의 패키지 경로는 아래와 같다.
package org.springframework.data.jpa.repository;
<출처 김영한: 실전! 스프링 데이터 JPA >
실전! 스프링 데이터 JPA - 인프런 | 강의
스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제공합니다.
www.inflearn.com
'Spring > SpringDataJPA' 카테고리의 다른 글
사용자 정의 리포지토리 구현 (0) | 2022.04.18 |
---|---|
JPA Hint & Lock (0) | 2022.04.17 |
벌크성 수정 쿼리(error 해결중) (0) | 2022.04.17 |
스프링 데이터 JPA 페이징과 정렬 (0) | 2022.04.17 |
순수 JPA 페이징과 정렬 (0) | 2022.04.17 |