[JPA] manytoone n+1 문제?

JPA에서 @manytoone으로 다른 entity와 join했을 경우 list를 출력하면, 

리스트를 한번 조회하고, join column의 id 수만큼 다시 select를 하게된다. 


Member.java

@Entity
@Getter
@Setter
public class Member {
@Id
@GeneratedValue
private Long id;
private String name;

@ManyToOne
@JoinColumn(name="team_id")
private Team team;
}

MemberRepo.java

public interface MemberRepo extends JpaRepository<Member, Long> {}

Team.java

@Entity
@Getter
@Setter
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
}

TeamRepo.java

public interface TeamRepo extends JpaRepository<Team, Long> {}


test 데이터를 넣고 조회를 해보면


MemberTest.java

@RunWith(SpringRunner.class)
@SpringBootTest
public class MemberTest {
@Autowired
MemberRepo memberRepo;

@Autowired
TeamRepo teamRepo;

@Before
@Transactional
public void setup() {
Team team = new Team();
team.setName("team-1");
team = teamRepo.save(team);

Team team2 = new Team();
team2.setName("team-2");
team2 = teamRepo.save(team2);

Member member = new Member();
member.setName("member-1");
member.setTeam(team);
memberRepo.save(member);

Member member1 = new Member();
member1.setName("member-2");
member1.setTeam(team2);
memberRepo.save(member1);
}

@Test
public void joinTest() {
List<Member> all = memberRepo.findAll();
for (Member member1 : all) {
System.out.println(member1.getTeam().getName());
}
}
}


조회 쿼리를 살펴보면 아래와 같이 member table을 한번 조회하고 team테이블을 2번 조회하여 리스트 결과를 만들어 준다. 

@DataJpaTest로 안하고 @SpringBootTest로 한 이유는 @DataJpaTest에 경우 -똑똑한 jpa가 저장한 값을 들고 있어서 

조인쿼리를 한방에 만들어 준다. join 테스트를 하고 싶을때는 springbootest로 하길 권장한다.


Hibernate: 

    select

        member0_.id as id1_0_,

        member0_.name as name2_0_,

        member0_.team_id as team_id3_0_ 

    from

        member member0_

Hibernate: 

    select

        team0_.id as id1_1_0_,

        team0_.name as name2_1_0_ 

    from

        team team0_ 

    where

        team0_.id=?

2019-03-20 00:48:57.513 TRACE 1102 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [14]

Hibernate: 

    select

        team0_.id as id1_1_0_,

        team0_.name as name2_1_0_ 

    from

        team team0_ 

    where

        team0_.id=?

2019-03-20 00:48:57.516 TRACE 1102 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [15]


지금은 데이터가 2건이라 2번이지만 더 복잡할경우 N+1만큼 조회하는 문제가 생긴다.

이문제를 해결하려면 결과값을 entity가 아닌 새로운 dto에 맵핑해주면 된다. 

맵핑하는 방법은 생각외로 간단한데 


우선 맵핑할 dto 클래스를 하나 생성한다


MemberMapping.java

public interface MemberMapping {
Long getId();
String getName();
Team getTeam();
}


그리고 MemberRepo에 method를 하나 추가한다. 

public interface MemberRepo extends JpaRepository<Member, Long> {
List<MemberMapping> findAllBy();
}


그리고 테스트 코드에 내용을 아래와 같이 변경한다. 


@Test
public void joinTest() {
System.out.println("-result entity-------------");
List<Member> all = memberRepo.findAll();
for (Member member1 : all) {
System.out.println(member1.getTeam().getName());
}
System.out.println("--------------------");

System.out.println("-result dto---------");
List<MemberMapping> memberAll = memberRepo.findAllBy();
for (MemberMapping member1 : memberAll) {
System.out.println(member1.getTeam().getName());
}
System.out.println("--------------------");
}


첫번째는 entity를 result로 맵핑하는 데이터고 두번쨰는 우리가 만들어준 mapping dto에 맵핑하는 결과이다. 

테스트를 돌려보면 result entity는 기존과 마찬가지로 n+1만큼 team에 대한 조회가 일어나고

result dto는 조인쿼리로 한번만 조회가 된다. 


-result entity-------------

2019-03-20 23:42:08.366  INFO 1022 --- [           main] o.h.h.i.QueryTranslatorFactoryInitiator  : HHH000397: Using ASTQueryTranslatorFactory

Hibernate: 

    select

        member0_.id as id1_0_,

        member0_.name as name2_0_,

        member0_.team_id as team_id3_0_ 

    from

        member member0_

Hibernate: 

    select

        team0_.id as id1_1_0_,

        team0_.name as name2_1_0_ 

    from

        team team0_ 

    where

        team0_.id=?

2019-03-20 23:42:08.445 TRACE 1022 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]

Hibernate: 

    select

        team0_.id as id1_1_0_,

        team0_.name as name2_1_0_ 

    from

        team team0_ 

    where

        team0_.id=?

2019-03-20 23:42:08.448 TRACE 1022 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [2]

team-1

team-2

--------------------

-result dto---------

Hibernate: 

    select

        member0_.id as col_0_0_,

        member0_.name as col_1_0_,

        team1_.id as col_2_0_,

        team1_.id as id1_1_,

        team1_.name as name2_1_ 

    from

        member member0_ 

    left outer join

        team team1_ 

            on member0_.team_id=team1_.id

team-1

team-2

--------------------


위와 같은 방법으로 Interface를 생성하면 entity클래스에서 필요한 값만 리턴해줄수가 있다. 


자세한 코드는 아래 github에 있습니다. 


https://github.com/barocoding/jpa-manytoone-join-example