OneToMany, ManyToOne 테스트
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Entity
public class Nationality {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
@OneToMany
private List<Player> players;
@Enumerated(EnumType.STRING)
private Continent continent;
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Player {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
private Nationality nationality;
@ManyToOne
private Club club;
@Enumerated(EnumType.STRING)
private PlayerStatus status;
}
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Entity
public class Nationality {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
@OneToMany
private List<Player> players;
@Enumerated(EnumType.STRING)
private Continent continent;
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Club {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany
private List<Player> players;
}
클래스가 다음과 같이 주어졌을 때 nationalityRepository.findAll(); 을 하게 되면 먼저 nationality를 모두 가져오고 nationality에 연관관계가 있는 Player를 join한다. Club과 nationality는 Player에 연관관계가 있으므로 left join 해서 가져온다. 쿼리를 확인하면 nationality의 개수만큼 player, nationality, club을 조회함을 알 수 있다. (1+N 번 쿼리)
select
n1_0.id,
n1_0.continent,
n1_0.name
from
nationality n1_0
select
p1_0.nationality_id,
p1_1.id,
c1_0.id,
c1_0.name,
p1_1.name,
n1_0.id,
n1_0.continent,
n1_0.name,
p1_1.status
from
nationality_players p1_0
join
player p1_1
on p1_1.id=p1_0.players_id
left join
club c1_0
on c1_0.id=p1_1.club_id
left join
nationality n1_0
on n1_0.id=p1_1.nationality_id
where
p1_0.nationality_id=1
select
p1_0.nationality_id,
p1_1.id,
c1_0.id,
c1_0.name,
p1_1.name,
n1_0.id,
n1_0.continent,
n1_0.name,
p1_1.status
from
nationality_players p1_0
join
player p1_1
on p1_1.id=p1_0.players_id
left join
club c1_0
on c1_0.id=p1_1.club_id
left join
nationality n1_0
on n1_0.id=p1_1.nationality_id
where
p1_0.nationality_id=2
하지만 결과는 다음과 같다.
[
{
"id": 1,
"name": "Korea",
"players": [],
"continent": "ASIA"
},
{
"id": 2,
"name": "Japan",
"players": [],
"continent": "ASIA"
},
{
"id": 3,
"name": "China",
"players": [],
"continent": "ASIA"
},
{
"id": 4,
"name": "USA",
"players": [],
"continent": "NORTH_AMERICA"
},
{
"id": 5,
"name": "Brazil",
"players": [],
"continent": "SOUTH_AMERICA"
},
{
"id": 6,
"name": "Germany",
"players": [],
"continent": "EUROPE"
},
{
"id": 7,
"name": "France",
"players": [],
"continent": "EUROPE"
},
{
"id": 8,
"name": "England",
"players": [],
"continent": "EUROPE"
},
{
"id": 9,
"name": "Spain",
"players": [],
"continent": "EUROPE"
},
{
"id": 10,
"name": "Italy",
"players": [],
"continent": "EUROPE"
}
]
Players에 값이 하나도 없다. 왜 그럴까?
그건 바로 Nationality와 Players에 foreign key가 무엇이다를 알려줘야 하는데 그것이 없었기 때문이다. 따라서 Nationality 의 List<Player> players를 아래와 같이 바꿔주면 조회가 된다.
@OneToMany(mappedBy = "nationality")
private List<Player> players;
하지만 이렇게 하고 nationalityRepository.findAll(); 조회를 하면 아래와 같이 무한으로 json값이 생기므로 response 객체를 내려줄 땐 dto를 이용하자.
[
{
"id": 1,
"name": "Korea",
"players": [
{
"id": 1,
"name": "Son",
"nationality": {
"id": 1,
"name": "Korea",
"players": [
{
"id": 1,
"name": "Son",
"nationality": {
그럼 N+1 문제는 어떻게 해결할까?
우선 Player 의 ManyToOne에 fetchType을 Lazy로 줘 보자. 기본값은 Eager이다.
@ManyToOne(fetch = FetchType.LAZY)
private Nationality nationality;
@ManyToOne(fetch = FetchType.LAZY)
private Club club;
그리고 나서 조회를 하면 아래와 같은 오류가 발생하는데 이는 Lazy Loading으로 인해 아직 초기화되지 않은 Club 의 Entity 프록시(ByteBuddy를 이용함)를 Jackson이 직렬화 하려고 하기 때문에 Type definition error 가 발생하는 것이다.
"org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]\\n\\tat org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:489)\\n\\tat org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:114)\\n\\tat org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.
따라서 일단 Club 에 @JsonIgnore 를 붙여주고 실행하면 에러 없이 N+1번 쿼리가 발생하는 것을 알 수 있다.
하지만 아직도 N+1 문제는 해결되지 않았다.
Fetch Join
NationalityRepository 에 다음과 같은 메서드를 추가하자.
@Query("select n from Nationality n join fetch n.players")
List<Nationality> findAllByFetchJoin();
이 방법을 통해 조회하면 쿼리가 한번만 나가는 것을 알 수 있다.
select
n1_0.id,
n1_0.continent,
n1_0.name,
p1_0.nationality_id,
p1_0.id,
p1_0.club_id,
p1_0.name,
p1_0.status
from
nationality n1_0
join
player p1_0
on n1_0.id=p1_0.nationality_id
players에 연관관계를 가진 club까지 불러오려면 아래와 같이 작성하면 된다.
@Query("select n from Nationality n join fetch n.players p join fetch p.club")
List<Nationality> findAllByFetchJoin();
select
n1_0.id,
n1_0.continent,
n1_0.name,
p1_0.nationality_id,
p1_0.id,
c1_0.id,
c1_0.name,
p1_0.name,
p1_0.status
from
nationality n1_0
join
player p1_0
on n1_0.id=p1_0.nationality_id
join
club c1_0
on c1_0.id=p1_0.club_id
Left Join을 하고 싶으면 left를 추가하면 된다,.
@Query("select n from Nationality n join fetch n.players p left join fetch p.club")
List<Nationality> findAllByFetchJoin();
select
n1_0.id,
n1_0.continent,
n1_0.name,
p1_0.nationality_id,
p1_0.id,
c1_0.id,
c1_0.name,
p1_0.name,
p1_0.status
from
nationality n1_0
join
player p1_0
on n1_0.id=p1_0.nationality_id
left join
club c1_0
on c1_0.id=p1_0.club_id