[개인프로젝트] JPA OneToOne N+1 문제
쿼리 리팩토링하다가 정산 배치관련해서 쿼리 보다가 이게 뭐지..?하는 쿼리들이 많았다.
나는 분명 fetch join했는데 N+1문제가 터졌다..이게 무슨일이지..?? 초반에는 그냥 넘겼다. 일단 프로젝트 완료기간이 5일남았기에 남은 기능을 완료시키기 위해 ... 근데 이제는 시간이 어느정도 있어 한번 쿼리좀 보자 이 느낌으로 고치고 있는데 ...역시 N+1이 문제가 있었구만
문제상황
정산하기 위해서 예약 table과 pay table을 fetch join하는 query를 날렸다.
return new JpaPagingItemReaderBuilder<AdjustWalkerInfo>()
.name("adjustReader")
.entityManagerFactory(entityManagerFactory)
.pageSize(chunkSize)
.queryString("SELECT NEW com.project.dogwalker.batch.adjust.dto.AdjustWalkerInfo(u, ph, w) "
+ "FROM WalkerReserveServiceInfo w "
+ "JOIN FETCH w.payHistory ph "
+ "JOIN FETCH w.walker u "
+ "WHERE w.status = :status "
+ "AND ph.payStatus = :payStatus")
.parameterValues(parameter)
.build();
근데 엥...?? 이게 뭐람..? 쿼리 로그를 보니
SELECT
NEW com.project.dogwalker.batch.adjust.dto.AdjustWalkerInfo(u, ph, w)
FROM
WalkerReserveServiceInfo w
Inner JOIN
PayHistory ph
ON w.reserveId = ph.reserveService.reserveId
JOIN
FETCH User u
ON w.walker.userId = u.userId
WHERE
w.status = :status
AND ph.payStatus = :payStatus */ select
u1_0.user_id,
u1_0.user_created_at,
u1_0.user_updated_at,
u1_0.user_email,
u1_0.user_lat,
u1_0.user_lnt,
u1_0.user_name,
u1_0.user_phone_number,
u1_0.user_role,
p1_0.pay_history_id,
p1_0.pay_created_at,
p1_0.user_id,
p1_0.pay_method,
p1_0.pay_price,
p1_0.pay_status,
p1_0.walker_reserve_service_id,
p1_0.pay_updated_at,
w1_0.walker_reserve_service_id,
w1_0.walker_reserve_service_created_at,
w1_0.customer_id,
w1_0.walker_service_date,
w1_0.walker_reserve_service_price,
w1_0.walker_service_status,
w1_0.walker_service_time_unit,
w1_0.walker_reserve_service_updated_at,
w1_0.walker_id
from
walker_reserve_service w1_0
join
pay_history p1_0
on w1_0.walker_reserve_service_id=p1_0.walker_reserve_service_id
join
users u1_0
on w1_0.walker_id=u1_0.user_id
where
w1_0.walker_service_status=?
and p1_0.pay_status=? limit ?,?
2023-12-17T18:26:21.794+09:00 DEBUG 11476 --- [ main] org.hibernate.SQL :
select
p1_0.pay_history_id,
p1_0.pay_created_at,
p1_0.user_id,
p1_0.pay_method,
p1_0.pay_price,
p1_0.pay_status,
p1_0.walker_reserve_service_id,
p1_0.pay_updated_at
from
pay_history p1_0
where
p1_0.walker_reserve_service_id=?
2023-12-17T18:26:21.796+09:00 DEBUG 11476 --- [ main] org.hibernate.SQL :
select
p1_0.pay_history_id,
p1_0.pay_created_at,
p1_0.user_id,
p1_0.pay_method,
p1_0.pay_price,
p1_0.pay_status,
p1_0.walker_reserve_service_id,
p1_0.pay_updated_at
from
pay_history p1_0
where
p1_0.walker_reserve_service_id=?
뒤 쿼리가 Payhistory N개 만큼 생기고 있다. 앙...?나는 fetch join했다고..
그래서 찾아보니 역시나 나의 무지에 의한 실수다
원인
일단 예약 테이블과 페이 테이블은 oneToone관계이다.
거기다가 나느 lazy모드로 해놨다.
페치조인의 정의를 살펴보자
A "fetch" join allows associations or collections of values to be initialized along with their parent objects using a single select.
페치 조인을 사용하면 1개의 select문을 사용하여 주인(부모) 객체와 함께 연관된 엔티티나 컬렉션의 값을 초기화 시킬 수 있다.
여기서 잘 봐야하는 점은 부모객체라는 점이다.
그래서 내 코드를 생각해보니 (일부만 가져왔다)
@Entity
public class WalkerReserveServiceInfo extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "walker_reserve_service_id")
private Long reserveId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id",nullable = false)
private User customer;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "walker_id",nullable = false)
private User walker;
@OneToOne(mappedBy = "reserveService",fetch = FetchType.LAZY)
private PayHistory payHistory;
}
@Entity
public class PayHistory extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "pay_history_id")
private Long payId;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User customer;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "walker_reserve_service_id")
private WalkerReserveServiceInfo reserveService;
}
요로코롬 되어있었다. 즉 Payhistory가 부모객체이다. 그러다보니 OneToOne관계에서 아무리 Lazy로 로딩해도 또는 fetch join을 해도 연관관계의 주인이 아닌곳에서 호출하면 즉시로딩되면서 PayHistory도 같이 로딩된 것이다.
그 이유는 뭘까??
그 이유는 프록시의 한계이다.
내 테이블을 기준으로 이야기 하자면 WalkerReserveServiceInfo DB에는 PayHistory에 대한 외래키가 없다. 즉 payhistory필드가 존재하지 않는다.
프록시는 지연로딩으로 설정이 되어있는 엔티티를 프록시로 감싸서 동작하지만 위에서의 필드가 없기에 null로 되어있는 부분을 프록시로 감쌀 수가 없다.
쉽게 말해 WalkerReserveServiceINfo가 어떤 PayHistory에 의해 참조되고 있는지 아예알 수 없기에 존재여부 확인을 위해 지연로딩으로 동작하지 않는 것이다.
해결상황
그래서 쿼리를 수정하엿다.
reader.setQueryString("SELECT NEW com.project.dogwalker.batch.adjust.dto.AdjustWalkerInfo(u, ph, w) "
+ "FROM PayHistory ph "
+ "JOIN FETCH ph.walkerReserveInfo w "
+ "JOIN User u On w.walker.userId = u.userId "
+ "WHERE w.status = :status "
+ "AND ph.payStatus = :payStatus");
PayHistory를 기준으로 walkerReserveInfo를 fetch join하였다.
SELECT
NEW com.project.dogwalker.batch.adjust.dto.AdjustWalkerInfo(u, ph, w)
FROM
PayHistory ph
JOIN
FETCH ph.walkerReserveInfo w
JOIN
User u
On w.walker.userId = u.userId
WHERE
w.status = :status
AND ph.payStatus = :payStatus */ select
u1_0.user_id,
u1_0.created_at,
u1_0.updated_at,
u1_0.user_email,
u1_0.user_lat,
u1_0.user_lnt,
u1_0.user_name,
u1_0.user_phone_number,
u1_0.user_role,
p1_0.pay_history_id,
p1_0.created_at,
p1_0.user_id,
p1_0.pay_method,
p1_0.pay_price,
p1_0.pay_status,
p1_0.updated_at,
w1_0.walker_reserve_service_id,
w1_0.created_at,
w1_0.customer_id,
w1_0.walker_service_date,
w1_0.walker_reserve_service_price,
w1_0.walker_service_status,
w1_0.walker_service_time_unit,
w1_0.updated_at,
w1_0.walker_id
from
pay_history p1_0
join
walker_reserve_service w1_0
on w1_0.walker_reserve_service_id=p1_0.walker_reserve_service_id
join
users u1_0
on w1_0.walker_id=u1_0.user_id
where
w1_0.walker_service_status=?
and p1_0.pay_status=? limit ?,?
메우 깔끔하게 한번에 조회가 되었다.
부모,자식 관계를 깊게 생각안해보고 사용한 나의 잘못.... 거기다가 fetch join남발한 나의 잘못이다...
https://cobbybb.tistory.com/15