본문 바로가기
개발/JAVA

JPA N+1, pageable, projection.

by Shuming 2022. 10. 28.
반응형

지난 게시물에서의 결과를 요약하면 아래와 같다.

N+1 문제 해결 @OneToMany @ManyToOne
@Query() 를 이용한 fetch join inner join 으로 fetch join 된다.
@EntityGraph left outer join
@Fetch(FetchMode.SUBSELECT) subselect 구문을 해결된다.

 

그리고 페이징을 더하면?

pageable 을 더하면? @OneToMany @ManyToOne
@Query() 를 이용한 fetch join X X
@EntityGraph X O
@Fetch(FetchMode.SUBSELECT) O X

O - 정상
X - 실패

 

그렇다면 되는 넘들로 각각 projection 까지 적용해보자


Projection 이란.

물론 이 또한 정리가 아주 잘된 블로그도 많고, 유튜브도 있으므로 내 빈약한 요약보다는 다른 글을 보시길 바란다.
공식페이지 설명 링크


jpa 에서 projection 의 방법으로는 3가지가 있다. 자세한 내용은 링크 참조하시고.. 대략적으로 설명하자면


1. 인터페이스 기반 Projection

1-1. close Projection 

- interface 설정하고 field를 네이밍룰에 맞게 설정(String getId() 요딴식 )하면 interface 모양으로 결과물 받아옴.

1-2. open Projection

- 위에껀 네이밍 룰에 딱 맞춰야 되는데 이건 안그래도 됨 그리고 막 @Value 어노테이션이랑 SpEL도 쓸 수 있음.


2. 클래스 기반 Projection

interface 에서 걍 Dto 로 바뀐게 끝


3. 다이내믹 Projection 

똑같은 find~~ 구문에 제네릭으로 클래스를 받아서 결과값을 반환할 수 있음.
Person person = personRepository.findByLastName("Doe", Person.class);
PersonView personView = personRepository.findByLastName("Doe", PersonView.class);
PersonDto personDto = personRepository.findByLastName("Doe", PersonDto.class);

요딴식.


 

여기서 나는 Close Projection 을 사용할꺼다.

근데 어노테이션을 더할꺼임.

나는 spring-data-rest 프로젝트라 @Projection을 쓸꺼다

이게 바로바로 rest 결과값으로 나와야하는데 jpa 에서 설명하는 projection이랑 자꾸 햇갈려서 삽질 너무 많이 했음. 

 


여기서부터는 삽질일기

 

Team 객체에서 OneToMany 를 @EntityGraph 으로 N+1 문제를 해결했다 치자.

Team은 아래처럼 생겨먹었다.

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "TEAM")
public class Team {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "team_id")
  private Long id;

  private String name;
  private String nick;
  private Date created;

  @OneToMany(mappedBy = "team", cascade = CascadeType.ALL)
  private List<Member> members;

}

 

그리고 인터페이스로는 name이랑 member 만 가져오고 싶다면

public interface TeamRepository extends JpaRepository<Team, Long> {
  @EntityGraph(attributePaths = "members")
  List<CustomTeam> findAll();
}

interface CustomTeam {
  String getName();
  Member getMembers();
}

이렇게 하면 될줄 알았는데.

findAll은 오버라이딩이 안된다..

ㅇㅋ 그래 인정함. 그래서 검색 조건을 바꿔줘야댐 이거부터가 불편해.

일단 바꿈.

@RepositoryRestResource 
public interface TeamRepository extends JpaRepository<Team, Long> {
  @EntityGraph(attributePaths = "members")
  List<CustomTeam> findByName(String id);
}

 

그래 그럼 내가 Name으로 search 들어가서 검색도 해줄게.

 

 

???

 

뭔데 

 

query specified join fetching, but the owner of the fetched association was not present in the select list  

패치 조인을 했는데 왜 소유자가 없대? 뭔말이야? 

라고하자 갓영한님께서 등장하시는데..


fetch join을 사용하는 이유는 엔티티 상태에서 엔티티 그래프를 참조하기 위해서 사용하는 것입니다. 따라서 당연히 엔티티가 아닌 DTO 상태로 조회하는 것은 불가능합니다.

이 경우 fetch join을 사용하지 마시고, 그냥 순수한 join을 사용하시면 원하는 결과를 얻을 수 있습니다^^


근데 그럼 n+1 문제가 발생할 여지가있더라도 그냥 조인을 해야하나?

ㅇㅋㅇㅋ 일단 알았음. @EntityGraph 로 패치 조인 했으니까 그거 없애면 되겠지?

 

@RepositoryRestResource 
public interface TeamRepository extends JpaRepository<Team, Long> {
  // @EntityGraph(attributePaths = "members")
  List<CustomTeam> findByName(String name);
}

interface CustomTeam {
  String getName();
  Member getMembers();
}

주석 슥슥

 

시발

???????????

Couldn't find PersistentEntity for type class 에러를 내뿜으며 죽어버렸다

뭐지?

혹시 하고 웹으로 접근하지 않고 테스트를 돌려봤다

------------------------
Hibernate: 
    select
        team0_.name as col_0_0_,
        members1_.member_id as col_1_0_,
        members1_.member_id as member_i1_0_,
        members1_.name as name2_0_,
        members1_.team_id as team_id3_0_ 
    from
        team team0_ 
    left outer join
        member members1_ 
            on team0_.team_id=members1_.team_id 
    where
        team0_.name=?
Hibernate: 
    select
        team0_.team_id as team_id1_7_0_,
        team0_.created as created2_7_0_,
        team0_.name as name3_7_0_,
        team0_.nick as nick4_7_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
------------------------

???????? 조회를 하고있네?

근데 따로?? 엥ㅇ???????

그럼 Member없는건 조회 되나?

@RepositoryRestResource
public interface TeamRepository extends JpaRepository<Team, Long> {
  // @EntityGraph(attributePaths = "members")
  List<CustomTeam> findByName(String name);
}

interface CustomTeam {
  String getName();
  // Member getMembers();
}

다시 주석 슥슥

어????????? 근데 어디서 많이 본건데????? 

findByName이면 단건 조회잖아

@RepositoryRestResource
public interface TeamRepository extends JpaRepository<Team, Long> {
  // @EntityGraph(attributePaths = "members")
  Optional<CustomTeam> findByName(String name);
}

interface CustomTeam {
  String getName();
  // Member getMembers();
}

옵셔널로 슥슥..

 

테스트 결과는?

어 ㅎㅇ 근데 너 link 정보는 엿바꿔 먹고 왔냐?

 

아씨 머지 .. 그럼 findAll이 없는게 문제였나?

@RepositoryRestResource
public interface TeamRepository extends JpaRepository<Team, Long> {
  // @EntityGraph(attributePaths = "members")
  List<CustomTeam> findAllByName(String name);
}

응 어림도 없어 꺼져

 

ㅈㅈ 못해먹겠다


 

OneToMany 삽질.

하................... ㅇㅋ.. 알았음.. 그럼 member에서 team 조회는 할 수 있지?

@RepositoryRestResource 
public interface MemberRepository extends JpaRepository<Member, Long> {
  List<CustomMember> findAllByName(String name);
}

interface CustomMember {
  String getId();
}

어 당연히 안돼

 

아 진자 ㅡㅡ

 

결론 - 조회 자체는 된다. 근데 spring-data-rest 으로는 바로 표현이 안댐. 갓댐


 

@Projection 어노테이션

 

결론을 보면 조회는 되는건데.. jpa는 되는건데.. 내가 하려던건 data-rest 였잖아 하고 도큐먼트를 뒤지는데

https://www.baeldung.com/spring-data-rest-projections-excerpts

 

Projections and Excerpts in Spring Data REST | Baeldung

Create custom views of entities in Spring Data REST using projections and excerpts.

www.baeldung.com

도큐먼트 : ㅎㅇ

 

아 ㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣ시ㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣ바ㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏ

프로젝션이 또 있었음ㅋ

 

어떻게 쓰냐면.


@ManyToOne Team 객체야 뭐 그대로고.

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "TEAM")
public class Team {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "team_id")
  private Long id;

  private String name;
  private String nick;
  private Date created;

  @OneToMany(mappedBy = "team", cascade = CascadeType.ALL)
  private List<Member> members;

}

 

요로코롬 projection 어노테이션을 달아두면

@Projection(name = "customTeam", types = Team.class)
interface CustomTeam {
  String getName();
}

 

http://localhost/smp/api/v1/teams 으로 접속 시

{
  "_embedded" : {
    "teams" : [ {
      "name" : "name1",
      "nick" : null,
      "created" : null,
      "_links" : {
        "self" : {
          "href" : "http://localhost/smp/api/v1/teams/1"
        },
        "team" : {
          "href" : "http://localhost/smp/api/v1/teams/1{?projection}",
          "templated" : true
        },
  ~중략

위 처럼 http://localhost/smp/api/v1/teams/1/members{?projection} 템플릿 옵션이 생성된다.

http://localhost/smp/api/v1/teams/1?projection=customTeam 들어가보면?  
(* 위 @Projection annotation 에서 지정한 name value 이다.)

{
  "name" : "name1",
  "_links" : {
    "self" : {
      "href" : "http://localhost/smp/api/v1/teams/1"
    },
    "team" : {
      "href" : "http://localhost/smp/api/v1/teams/1{?projection}",
      "templated" : true
    },
    "members" : {
      "href" : "http://localhost/smp/api/v1/teams/1/members{?projection}",
      "templated" : true
    }
  }
}

짠. 내가 원하는 결과값이 나온다.

 

JOIN 된 결과값을 받아오려면

@Projection(name = "customTeam", types = Team.class)
interface CustomTeam {
  String getName();
  Member getMembers();
}

이렇게 작성할 수 있겠다.

 

결과 -

{
  "name" : "name1",
  "members" : {
    "name" : "name member ?"
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost/smp/api/v1/teams/1"
    },
    "team" : {
      "href" : "http://localhost/smp/api/v1/teams/1{?projection}",
      "templated" : true
    },
    "members" : {
      "href" : "http://localhost/smp/api/v1/teams/1/members{?projection}",
      "templated" : true
    }
  }
}

 

항상 url 에 projection query string 을 붙여서 조회를 해야하나?

Nope


RepositoryRestResource - excerptProjection

소제목처럼 excerptProjection 옵션으로 매 리스트 조회 시 마다 내가 원하는 projection을 붙여줄 수 있다.

@RepositoryRestResource(excerptProjection = CustomTeam.class)
public interface TeamRepository extends JpaRepository<Team, Long> {
}

@Projection(name = "customTeam", types = Team.class)
interface CustomTeam {
  String getName();
  Member getMembers();
}

excerptProjection 옵션 value 는 interface class 이다.

{
  "_embedded" : {
    "teams" : [ {
      "name" : "name1",
      "members" : {
        "name" : "name member ?"
      },
      "_links" : {
        "self" : {
          "href" : "http://localhost/smp/api/v1/teams/1"
        },
        "team" : {
          "href" : "http://localhost/smp/api/v1/teams/1{?projection}",
          "templated" : true
        },
        "members" : {
          "href" : "http://localhost/smp/api/v1/teams/1/members{?projection}",
          "templated" : true
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost/smp/api/v1/teams"
    },
    "profile" : {
      "href" : "http://localhost/smp/api/v1/profile/teams"
    }
  },
  "page" : {
    "size" : 10,
    "totalElements" : 1,
    "totalPages" : 1,
    "number" : 0
  }
}

 

결과 이렇게 나온다.

 

 

 

이제 진짜 끝.

원하는 결과 조물조물 만들 수 있게 됐으니 일단 사이드 프로젝트를 진행하고. 추후에 JPA는 갓영한님 강의를 들으면서 기초를 다시 다질 예정이다. 지금은 JPA 찍먹만 해보는중..

 

 

반응형

'개발 > JAVA' 카테고리의 다른 글

JPA n+1 정리 page, projection.  (0) 2022.10.27
JPA N:1 단방향 맵핑  (0) 2022.10.26
JPA 1:N 양방향 맵핑  (0) 2022.10.26
JPA 1:N 단방향 맵핑  (0) 2022.10.26
Tomcat 에 사설 SSL 적용하기 (https 적용)  (2) 2018.02.13

댓글