작성
·
221
·
수정됨
0
[질문 내용]
안녕하세요! @OneToMany지연로딩 관련해서 질문 드립니다.
아래는 Team, Member 엔티티로, 연관관계를 갖습니다. (1:N)
team 엔티티
// Team.java
package hellojpa;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
private List<Member> members = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Member> getMembers() {
return members;
}
public void setMembers(List<Member> members) {
this.members = members;
}
}
member.class , 엔티티
// Member.java
package hellojpa;
import jakarta.persistence.*;
import javax.xml.namespace.QName;
import java.util.Date;
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Team getTeam() {
return team;
}
public void setTeam(Team team) {
this.team = team;
}
}
위의 team ,member에서는 @OneToMany인데, 아래 코드 작동 시, 프록시 객체들이 조회안되고 진짜 엔티티가 조회되어 지연로딩이 발생 안합니다.
package hellojpa;
import jakarta.persistence.*;
import java.util.List;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
//code
EntityTransaction tx = em.getTransaction();
tx.begin();
try{
//저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member1 = new Member();
Member member2 = new Member();
member1.setUsername("member1");
member1.setTeam(team);
member2.setUsername("member2");
member2.setTeam(team);
em.persist(member1);
em.persist(member2);
em.flush();
em.clear();
Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers(); // 이 부분에서 프록시 객체로 조회가 되지 않습니다.!
for (Member m : members) {
System.out.println(m.getClass()); // member.class로 콘솔 출력 됩니다..
System.out.println(m.getUsername());
}
tx.commit();
} catch (Exception e){
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
아래는 위의 코드 실행 시 콘솔 창입니다.
Hibernate:
create sequence Member_SEQ start with 1 increment by 50
Hibernate:
create sequence Team_SEQ start with 1 increment by 50
Hibernate:
create table Member (
MEMBER_ID bigint not null,
TEAM_ID bigint,
USERNAME varchar(255),
primary key (MEMBER_ID)
)
5월 15, 2024 12:26:48 오후 org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl getIsolatedConnection
INFO: HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@1fbf088b] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode.
Hibernate:
create table Team (
TEAM_ID bigint not null,
name varchar(255),
primary key (TEAM_ID)
)
Hibernate:
alter table if exists Member
add constraint FKl7wsny760hjy6x19kqnduasbm
foreign key (TEAM_ID)
references Team
Hibernate:
select
next value for Team_SEQ
Hibernate:
select
next value for Member_SEQ
Hibernate:
select
next value for Member_SEQ
Hibernate:
/* insert for
hellojpa.Team */insert
into
Team (name, TEAM_ID)
values
(?, ?)
Hibernate:
/* insert for
hellojpa.Member */insert
into
Member (TEAM_ID, USERNAME, MEMBER_ID)
values
(?, ?, ?)
Hibernate:
/* insert for
hellojpa.Member */insert
into
Member (TEAM_ID, USERNAME, MEMBER_ID)
values
(?, ?, ?)
///////////////// ///////////////// /////////////////
Hibernate:
select
t1_0.TEAM_ID,
t1_0.name
from
Team t1_0
where
t1_0.TEAM_ID=?
Hibernate:
select
m1_0.TEAM_ID,
m1_0.MEMBER_ID,
m1_0.USERNAME
from
Member m1_0
where
m1_0.TEAM_ID=?
// 실제 객체
class hellojpa.Member
member1
class hellojpa.Member
member2
for-each로 member 클래스를 출력했을 때, 프록시 객체로 조회가 되지 않으며, team.getMembers()를 실행할 때 in절로 여러개의 members엔티티를 조회해 오는 것 같습니다..
제가 강의를 통해 이해한 바로는, @OneToMany는 기본적으로 지연로딩이 걸려 있어, 컬렉션을 조회할 때 각 엔티티들은 '프록시'로 조회되고(지연로딩) , 각 컬렉션의 객체들에 접근할 때 추가적인 (select 문) 조회 쿼리가 발생하여 N+1문제를 낳는다고 알고 있습니다..
-아래는 후반부의 강의 코드
- 강의상 지연이 발생 하는 코드 => OrderItemDto에서 N+1쿼리 발생
package jpabook.jpashop.api;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderItem;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.repository.OrderRepository;
import jpabook.jpashop.repository.OrderSearch;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequiredArgsConstructor
public class OrderApiController {
private final OrderRepository orderRepository;
@GetMapping("/api/v1/orders")
public List<Order> ordersV1() {
List<Order> all = orderRepository.findAllByString(new OrderSearch());
for (Order order : all) {
order.getMember().getName(); //Lazy 강제 초기화
order.getDelivery().getAddress(); //Lazy 강제 초기환
List<OrderItem> orderItems = order.getOrderItems();
orderItems.stream().forEach(o -> o.getItem().getName()); //Lazy 강제
}
return all;
}
@GetMapping("/api/v2/orders")
public List<OrderDto> ordersV2(){
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
List<OrderDto> collect = orders.stream()
.map(o -> new OrderDto(o))
.collect(Collectors.toList());
return collect;
}
@Data
static class OrderDto{
private Long orderId;
private String name;
private LocalDateTime orderDate;
private OrderStatus orderStatus;
private Address address;
private List<OrdereItemDto> orderItems;
public OrderDto(Order o) {
orderId = o.getId();
name=o.getMember().getName();
orderDate=o.getOrderDate();
orderStatus=o.getStatus();
address=o.getDelivery().getAddress();
orderItems = o.getOrderItems().stream()
.map(orderItem -> new OrdereItemDto(orderItem))
.collect(Collectors.toList());
}
}
@Getter
static class OrdereItemDto{
private String itemName; //상품명
private int orderPrice; //주문 가격
private int count; //주문 수량
public OrdereItemDto(OrderItem orderItem) {
itemName=orderItem.getItem().getName(); //문제 상황, 지연로딩 발생
orderPrice=orderItem.getItem().getPrice();
count = orderItem.getCount();
}
}
}
- Order, OrderItems에서도 @OneToMany인데, 지연로딩이 발생하여, orderItems 각각의 필드값을 조회시 N+1쿼리가 나가는 것이 확인되어, 차이점이 무엇인지 알고 싶습니다.
@GetMapping("/api/v1/orders")
public List<Order> ordersV1() {
List<Order> orders = orderRepository.findAllByString(new OrderSearch());
// 지연로딩 데이터 가져오기
for (Order order : orders) {
order.getMember().getName(); // 지연로딩 초기화
order.getDelivery().getAddress(); // 지연로딩 초기화
// 2. orderItem -> getClass()
for(OrderItem o : orderItems) {
System.out.println(o.getClass()) //프록시객체
}
//3. 여기서는 select 나가서 진짜 엔티티 갖고 오는거
orderItems.stream().forEach(orderItem -> orderItem.getItem().getName()); // 상품명을 가져오기 위해서 지연로딩 강제 초기화
}
return orders;
}}
@OneToMany를 걸 경우, 기본 전략이 lazyLoading으로 알고 있습니다..
이런 상황에서 getEntityList를 할 때, 프록시 객체가 아니라, 왜 한꺼번에 엔티티를 들고오는지 궁굼합니다..!
답변 1
0
안녕하세요. 깨위님, 공식 서포터즈 코즈위버입니다.
쿼리 실행 내용을 확인해보면 아래와 같이 Team 에 대한 조회가 먼저 발생하고,
그 이후 Member에 대한 실사용이 발생(for 문안에서 member.getClassName() / member.getName()) 할 때 Member 테이블을 다시 조회하고 있는것을 알 수 있습니다. 지연로딩이 적용된 것으로 보입니다.
Hibernate:
select
t1_0.TEAM_ID,
t1_0.name
from
Team t1_0
where
t1_0.TEAM_ID=?
Hibernate:
select
m1_0.TEAM_ID,
m1_0.MEMBER_ID,
m1_0.USERNAME
from
Member m1_0
where
m1_0.TEAM_ID=?
감사합니다.
답변 감사합니다!
그런데 강의에서는 조회 쿼리문이 2개 발생할 경우 지연로딩인 것으로 이해했는데, 본 질문에서는 1개 발생하고 있습니다.
또한 지연로딩 클래스를 조회할때도 프록시값이 아닌 엔티티가 조회될 경우 지연로딩이 아니지 않나요?
잘못 이해한 부분이 있는지 확인 부탁드립니다!