이번 포스팅에선 fetch join, entityGraph를 사용할때 중복데이터를 왜 제거해야 하는지 알아보도록 하겠습니다.(근데 entityGraph에선 학습테스트때 중복데이터가 발생하지 않더라고요.. 왜 그런지는 아직은 잘 모르겠습니다..)
이전 N+1 정복기 기본편, N+1 정복기 기본편2에서 모두 fetch join일 때 쿼리에 distinct
를 붙였습니다. 왜 distinct
키워드를 붙여야 하는지, 붙이지 않으면 어떤 문제가 발생할 수 있는지 학습테스트를 통해 알아보겠습니다.
fetch join에서는 inner join, EntityGraph에서는 outer join이 일어납니다. 두 join 모두 카테시안 곱(Cartesian Product)가 발생해 데이터가 중복되는 문제가 발생합니다.
10개의 team에 각각 2개의 member를 넣고 모든 team을 조회하는 학습테스트를 통해 카테시안 곱을 확인해보겠습니다.
Team3
@Entity
public class Team3 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
@Column(name = "team_name")
private String name;
@OneToMany(fetch = FetchType.LAZY, cascade = {PERSIST, REMOVE})
@JoinColumn(name = "team_id")
private List<Member3> member3s = new ArrayList<>();
protected Team3() {
}
public Team3(String name, List<Member3> member3s) {
this.name = name;
this.member3s = member3s;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public List<Member3> getMembers() {
return member3s;
}
}
Repository
@Query(value = "SELECT t FROM Team3 t join fetch t.member3s")
List<Team3> findAllWithFetchJoin();
학습 테스트
@BeforeEach
void setUp() {
for (int i = 0; i<10; ++i) {
Member3 member = new Member3("member" + i);
Member3 member2 = new Member3("member2" + i);
List<Member3> members = List.of(member, member2);
Team3 team = new Team3("team" + i, members);
team3Repository.save(team);
em.flush();
em.clear();
}
}
@DisplayName("카테시안 곱이 발생하는 fetch join")
@Test
void fetchJoin_CartesianProduct() {
List<Team3> teams = team3Repository.findAllWithFetchJoin();
for (Team3 team : teams) {
assertThat(team.getMembers()).hasSize(2);
}
assertThat(teams).hasSize(20);
}
분명 팀은 10개인데, 2개의 멤버가 들어간 20개의 팀이 조회되는 것을 알 수 있습니다. 즉 데이터가 중복으로 조회되는 문제가 발생합니다.
repository의 @Query에 DISTINCT를 사용하면 중복되는 데이터를 제거할 수 있습니다.
Repository
@Query(value = "SELECT DISTINCT t FROM Team3 t join fetch t.member3s")
List<Team3> findAllWithDistinctFetchJoin();
학습 테스트
@DisplayName("sql에 distinct를 사용해 중복 데이터 제거")
@Test
void fetchJoin_Distinct() {
List<Team3> teams = team3Repository.findAllWithDistinctFetchJoin();
for (Team3 team : teams) {
assertThat(team.getMembers()).hasSize(2);
}
assertThat(teams).hasSize(10);
}
Team엔티티에서 Member를 Set에 저장하면 중복 데이터를 제거할 수 있습니다.
Team3
@Entity
public class Team3 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
@Column(name = "team_name")
private String name;
@OneToMany(fetch = FetchType.LAZY, cascade = {PERSIST, REMOVE})
@JoinColumn(name = "team_id")
private Set<Member3> member3s = new HashSet<>();
protected Team3() {
}
public Team3(String name, Set<Member3> member3s) {
this.name = name;
this.member3s = member3s;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public Set<Member3> getMembers() {
return member3s;
}
}
학습 테스트
@DisplayName("java의 set을 사용해 중복 데이터 제거")
@Test
void fetchJoin_Set() {
List<Team3> teams = team3Repository.findAllWithDistinctFetchJoin();
for (Team3 team : teams) {
assertThat(team.getMembers()).hasSize(2);
}
assertThat(teams).hasSize(10);
}
Team4
@Entity
public class Team4 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
@Column(name = "team_name")
private String name;
@OneToMany(fetch = FetchType.LAZY, cascade = {PERSIST, REMOVE})
@JoinColumn(name = "team_id")
private List<Member4> member4s = new ArrayList<>();
protected Team4() {
}
public Team4(String name, List<Member4> member4s) {
this.name = name;
this.member4s = member4s;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public List<Member4> getMembers() {
return member4s;
}
}
Repository
@EntityGraph(attributePaths = "member4s")
@Query(value = "SELECT t FROM Team4 t")
List<Team4> findAllEntityGraph();
학습 테스트
@BeforeEach
void setUp() {
for (int i = 0; i < 10; ++i) {
Member4 member = new Member4("member" + i);
Member4 member2 = new Member4("member2" + i);
List<Member4> members = List.of(member, member2);
Team4 team = new Team4("team" + i, members);
team4Repository.save(team);
em.flush();
em.clear();
}
}
@DisplayName("entityGraph 카테시안 곱으로 인한 중복 데이터 확인")
@Test
void entityGraph_CartesianProduct() {
List<Team4> teams = team4Repository.findAll();
for (Team4 team : teams) {
assertThat(team.getMembers()).hasSize(2);
}
assertThat(teams).hasSize(10);
}
이상하게 entityGraph에선 DISTINCT
, Set
을 사용하지 않아도 중복 데이터가 발생하지 않았다..
왜 그런지 아직은 잘 모르겠다. 추후 알게되면 추가하겠습니다..