반응형
Entity Graph
- Entity를 조회하는 시점에 연관 Entity들을 함께 조회할 수 있도록 해주는 기능
종류
- 정적 선언 - @NamedEntityGraph
- 동적 선언 - EntityManager.createEntityGraph()
@NamedEntityGraphs({
@NamedEntityGraph(name = "orderWithCustomer", attributeNodes = { //같이 가져와
@NamedAttributeNode("customer")
}),
@NamedEntityGraph(name = "orderWithOrderItems", attributeNodes = {
@NamedAttributeNode("orderItems")
}),
@NamedEntityGraph(name = "orderWithCustomerAndOrderItems", attributeNodes = {
@NamedAttributeNode("customer"),
@NamedAttributeNode("orderItems")
}),
@NamedEntityGraph(name = "orderWithCustomerAndOrderItemsAndItem", attributeNodes = {
@NamedAttributeNode("customer"),
@NamedAttributeNode(value = "orderItems", subgraph = "orderItems")
}, subgraphs = @NamedSubgraph(name = "orderItems", attributeNodes = { //orderItem 한 단계 더 들어갈 때
@NamedAttributeNode("item")
}))
})
@Getter
@Setter
@Entity
@Table(name = "Orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_id")
private Long orderId;
@ManyToOne(optional = false)
@JoinColumn(name = "customer_id")
private Customer customer;
@Column(name = "order_dt")
private LocalDateTime orderDate;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
private List<OrderItem> orderItems;
}
public interface OrderRepository extends JpaRepository<Order, Long> {
//get, read, query, find -> select문
//All -> 의미 없음; 뭘 가져올지는 return type으로
//by -> 조건
@EntityGraph("orderWithCustomer")
//select * from order left join customer -> 커스토머 가져오고
//select * from orderItem left join item -> 추가로 발생
List<Order> getAllBy();
@EntityGraph("orderWithOrderItems")
//select * from order left join orderItem -> 한방에
List<Order> readAllBy();
@EntityGraph("orderWithCustomerAndOrderItems")
//select * from order left join customer left join orderItem -> 한방에
List<Order> queryAllBy();
@EntityGraph("orderWithCustomerAndOrderItemsAndItem")
//select * from order left join customer left join orderItem left join item -> 한방에
List<Order> findAllBy();
}
Pagination 쿼리에 Fetch Join
Pagination 쿼리에 Fetch Join을 적용하면 실제로는 모든 레코드를 가져오는 쿼리가 실행된다
: 다 가져와서 필요한 부분만 발라서 줌
: DB 서버는 전체를 부르게 되니 부하 오짐
실제로는 에러가 아닌 warning에 아래와 같은 메세지가 나고 있었다..
디비에서는 다가져와서 메모리에서 할게 ㅎㅎ
HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
public Page<Order> getPagedOrderWithAssociations(Pageable pageable) {
QOrder order = QOrder.order;
QCustomer customer = QCustomer.customer;
QOrderItem orderItem = QOrderItem.orderItem;
QItem item = QItem.item;
// TODO : pagination + fetch join ???
//사라진 limit
JPQLQuery<Order> query = from(order)
.innerJoin(order.customer, customer).fetchJoin()
.leftJoin(order.orderItems, orderItem).fetchJoin()
.innerJoin(orderItem.item, item).fetchJoin();
JPQLQuery<Order> pagedQuery = getQuerydsl().applyPagination(pageable, query);
long totalCount = 0L;
try {
totalCount = pagedQuery.fetchCount();
} catch (NoResultException e) {
// ignore
}
List<Order> list = pagedQuery.fetch();
return new PageImpl<>(list, pageable, totalCount);
}
해결 방법? 정해진 해결방법은 없음.
- Pagination 쿼리와 Fetch Join을 분리
- Pagination 쿼리에서는 entity가 아닌 ID만을 가져온다(where절을 만족하는 놈으로 미리 쳐버리는게 효율이 좋겠지)
- Fetch Join에는 앞서 가져온 ID 조건을 추가
Q. 이럴 때 in절로 인한 디비 부하가 있을 수 있을텐데.. 괜찮나? 물론 전체를 가져오는 것 보다는 나을 것이며 PK니까 관련 인덱스도 있겠지만... DBA는 in절을 매우 싫어했는데ㅠ
public Page<Order> getPagedOrderWithAssociations(Pageable pageable) {
QOrder order = QOrder.order;
QCustomer customer = QCustomer.customer;
QOrderItem orderItem = QOrderItem.orderItem;
QItem item = QItem.item;
// TODO #1 : pagination query
//여기서 where절 미리 쳐버렸!
JPQLQuery<Long> query = from(order).select(order.orderId);
JPQLQuery<Long> pagedQuery = getQuerydsl().applyPagination(pageable, query);
long totalCount = 0L;
try {
totalCount = pagedQuery.fetchCount();
} catch (NoResultException ex) {
// ignore
}
List<Long> ids = pagedQuery.fetch();
// TODO #2 : fetch join
List<Order> list = from(order)
.innerJoin(order.customer, customer).fetchJoin()
.leftJoin(order.orderItems, orderItem).fetchJoin()
.innerJoin(orderItem.item, item).fetchJoin()
.where(order.orderId.in(ids))
.fetch();
return new PageImpl<>(list, pageable, totalCount);
}
cf.) pagination query 에서 offset, limit 에 bind 된 parameter 값은 왜 log에 안 나오죠?
limit 뒤의 값을 알고싶어요..
select
order0_.order_id as order_id1_3_,
order0_.customer_id as customer3_3_,
order0_.order_dt as order_dt2_3_
from orders order0_
where
order0_.order_id=?
limit ?
o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1]
이유?
- org.hibernate.type.descriptor.sql.BasicBinder
- bind 된 parameter 값을 logging
- offset, limit 는 DBMS 별로 지원이 될 수도 있고 안 될 수도 있는 쿼리
- cf.) org.hibernate.dialect.pagination.LimitHandler
- MySQLDialect vs Oracle8iDialect
- cf.) org.hibernate.dialect.pagination.LimitHandler
- offset, limit 는 BasicBinder 에서 처리가 되지 않음
- dialect 에서도 logging을 해주지 않고 있음
package org.hibernate.dialect.pagination;
...
public interface LimitHandler {
boolean supportsLimit(); //각 dbms에서 구현
boolean supportsLimitOffset();
public class MySQLDialect extends Dialect {
private static final Pattern ESCAPE_PATTERN = Pattern.compile("\\", 16);
public static final String ESCAPE_PATTERN_REPLACEMENT = Matcher.quoteReplacement("\\\\");
private final UniqueDelegate uniqueDelegate;
private final MySQLStorageEngine storageEngine;
private static final LimitHandler LIMIT_HANDLER = new AbstractLimitHandler() {
public String processSql(String sql, RowSelection selection) {
boolean hasOffset = LimitHelper.hasFirstRow(selection);
return sql + (hasOffset ? " limit ?, ?" : " limit ?");
}
public boolean supportsLimit() {
return true;
}
};
ㅋ 구현체에서 안 찍어줌
해결방법
- 굳이 offset, limit 값을 로깅하길 원한다면
- log4jdbc와 같은 JDBC 레벨에서의 로깅이 가능한 라이브러리를 써야
둘 이상의 컬렉션을 Fetch Join하는 경우
- Order Entity에 OrderAttribute Entity로의 일대다(1:N) 연관관계를 추가하는 경우
- Order-OrderDetails (1:N)
- Order-OrderAttributes (1:N)
둘 이상의 컬렉션을 Fetch Join하는 경우 MultipleBagFetchException 발생
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
private List<OrderItem> orderItems;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "order_id")
private List<OrderAttribute> attributes;
//
public List<Order> getOrdersWithAssociations() {
QOrder order = QOrder.order;
QCustomer customer = QCustomer.customer;
QOrderItem orderItem = QOrderItem.orderItem;
QItem item = QItem.item;
QOrderAttribute attribute = QOrderAttribute.orderAttribute;
return from(order)
.innerJoin(order.customer, customer).fetchJoin()
.leftJoin(order.orderItems, orderItem).fetchJoin()
.leftJoin(order.attributes, attribute).fetchJoin()
.innerJoin(orderItem.item, item).fetchJoin()
.fetch();
}
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags:
[com.nhn.edu.jpa.entity.Order.orderItems, com.jpa.entity.Order.attributes]
트러블 슈팅 - 둘 이상의 컬렉션을 Fetch Join하는 경우
MultipleBagFetchException
- Hibernate는 collection type으로 list, set, bag, map 등 다양한 타입을 지원
- Java의 java.util.List 타입은 기본적으로 Hibernate의 Bag 타입으로 맵핑됨
- Bag은 Hibernate에서 중복 요소를 허용하는 비순차(unordered) 컬렉션
- 둘 이상의 컬렉션(Bag)을 Fetch Join하는 경우, 그 결과로 만들어지는 카테시안 곱(Cartesian Product)에서
어느 행이 유효한 중복을 포함하고 있고 어느 행이 그렇지 않은 지 판단할 수 없어 Bag 컬렉션으로 변환될 수 없기 때문에 MultipleBagFetchException 예외 발생- 조인 할 때 row에 옆으로 쫙 늘어나면서 데이터가 n*n으로 나올거잖슴,, 반복되는 내용 때문인지 중복이 왜 나는지 구분이 안되니
- 애초에 결과 값에 중복 허용 안하면 카테시안 곱에 의한 중복이 아니라는게 확실해짐
해결 방법
1. List를 Set으로 변경 : 중복 비허용
- 도메인이나 비즈니스 로직에 따라 중복이 나오는게 맞는 것인지 고려해보고 적용
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
private Set<OrderItem> orderItems;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "order_id")
private Set<OrderAttribute> attributes;
//
@Override
public List<Order> getOrdersWithAssociations() {
QOrder order = QOrder.order;
QCustomer customer = QCustomer.customer;
QOrderItem orderItem = QOrderItem.orderItem;
QItem item = QItem.item;
QOrderAttribute attribute = QOrderAttribute.orderAttribute;
return from(order)
.innerJoin(order.customer, customer).fetchJoin()
.leftJoin(order.orderItems, orderItem).fetchJoin()
.leftJoin(order.attributes, attribute).fetchJoin()
.innerJoin(orderItem.item, item).fetchJoin()
.fetch();
}
2. @OrderColumn 적용 : 순서를 부여(ordered)
- 디비 스키마를 변경할 수 있는지 고려해야함
WARN QueryTranslatorImpl : HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
@OrderColumn //순서를 부여함; 중복 허용
private List<OrderItem> orderItems;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "order_id")
@OrderColumn
private List<OrderAttribute> attributes;
Hibernate:
create table order_attributes (
order_attribute_id bigint generated by default as identity,
attribute varchar(255),
order_id bigint,
attributes_order integer, ///순서를 저장하기 위한 콜롬이 추가로 생성된
primary key (order_attribute_id)
)
Hibernate:
create table order_items (
order_line_id bigint generated by default as identity,
quantity bigint,
item_id bigint,
order_id bigint,
order_items_order integer, ///순서를 저장하기 위한 콜롬이 추가로 생성된
primary key (order_line_id)
)
1, 2번으로 해결이 안되면 Jpa를 쓰지말자! ㅋㅋ
Repository: spring이 제공하는 데이터에 접근하는 layer
- Spring Data Repository
- Repository는 JPA의 개념이 아니고, Spring Framework가 제공해주는 것임.
- data access layer 구현을 위해 반복해서 작성했던, 유사한 코드를 줄일 수 있는 추상화 제공
이름 규칙으로 join 쿼리 가져오기(N+1 날 수 있음)
public interface MemberRepository extends JpaRepository<Member, Long> {
// select m from Member m inner join MemberDetail md where md.type = ?
// 연관관계가 있을때만 가능
// _ 로 안으로 들어갈 수 있음
List<Member> findByDetails_Pk_Type(String type);
}
DTO Projection
DTO Projection 이란: entity 바를 때
- Repository 메서드가 Entity를 반환하는 것이 아니라 원하는 필드만 뽑아서 DTO(Data Transfer Object)로 반환하는 것
- 메모리나 디비 성능에 도움
Dto Projection 방법
- Interface 기반 Projection: 아래에 계속!
- Class 기반 (DTO) Projection: 생성자
- Dynamic Projection: runtime에 결정
트러블 슈팅 - Spring Data Repository 로는 Dto Projection을 할 수 없다?
Spring Data Repository를 이용한 Dto Projection
- Repository 메서드의 반환 값으로 Entity가 아닌 Dto를 사용할 수 있다
- interface / class
- @Value + SpEL (target)
public interface OrderRepository extends OrderRepositoryCustom, JpaRepository<Order, Long> {
List<OrderDto> findAllBy();
}
public interface OrderDto {
Long getOrderId();
CustomerDto getCustomer();
List<OrderItemDto> getOrderItems();
interface CustomerDto {
String getCustomerName();
}
interface OrderItemDto {
ItemDto getItem();
Long getQuantity();
}
interface ItemDto {
String getItemName();
Long getItemPrice();
}
}
Hibernate:
select
order0_.order_id as order_id1_3_,
order0_.customer_id as customer3_3_,
order0_.order_dt as order_dt2_3_
from
orders order0_
Hibernate:
select
customer0_.customer_id as customer1_0_0_,
customer0_.customer_name as customer2_0_0_
from
customers customer0_
where
customer0_.customer_id=?
Hibernate:
select
customer0_.customer_id as customer1_0_0_,
customer0_.customer_name as customer2_0_0_
from
customers customer0_
where
customer0_.customer_id=?
Hibernate:
select
orderitems0_.order_id as order_id4_2_0_,
orderitems0_.order_line_id as order_li1_2_0_,
orderitems0_.order_line_id as order_li1_2_1_,
orderitems0_.item_id as item_id3_2_1_,
orderitems0_.quantity as quantity2_2_1_,
item1_.item_id as item_id1_1_2_,
item1_.item_name as item_nam2_1_2_,
item1_.item_price as item_pri3_1_2_
from
order_items orderitems0_
left outer join
items item1_
on orderitems0_.item_id=item1_.item_id
where
orderitems0_.order_id=?
Hibernate:
select
orderitems0_.order_id as order_id4_2_0_,
orderitems0_.order_line_id as order_li1_2_0_,
orderitems0_.order_line_id as order_li1_2_1_,
orderitems0_.item_id as item_id3_2_1_,
orderitems0_.quantity as quantity2_2_1_,
item1_.item_id as item_id1_1_2_,
item1_.item_name as item_nam2_1_2_,
item1_.item_price as item_pri3_1_2_
from
order_items orderitems0_
left outer join
items item1_
on orderitems0_.item_id=item1_.item_id
where
orderitems0_.order_id=?
Hibernate:
select
orderitems0_.order_id as order_id4_2_0_,
orderitems0_.order_line_id as order_li1_2_0_,
orderitems0_.order_line_id as order_li1_2_1_,
orderitems0_.item_id as item_id3_2_1_,
orderitems0_.quantity as quantity2_2_1_,
item1_.item_id as item_id1_1_2_,
item1_.item_name as item_nam2_1_2_,
item1_.item_price as item_pri3_1_2_
from
order_items orderitems0_
left outer join
items item1_
on orderitems0_.item_id=item1_.item_id
where
orderitems0_.order_id=?
하지만 N+1은 난다.
예제
- interface / class
- @Value + SpEL (target)
/*
{
"name": "",
"details": [{
"type": "",
"description": ""
}]
}
*/
public interface MemberDto {
String getName();
List<MemberDetailDto> getDetails();
interface MemberDetailDto{
//target = memberDetailEntity
@Value("#{target.pk.type}") ///!
String getType();
String getDescription();
}
//아래처럼 하면 pk 안의 type으로 나옴
// interface MemberDetailDto{
// PkDto getPk();
// String getDescription();
//
// interface PkDto{
// String getType();
// }
// }
}
728x90
반응형
'개발 > spring' 카테고리의 다른 글
[mapper] mapstruct vs modelmapper vs copyproperties (0) | 2023.03.24 |
---|---|
[문제해결] Execution failed for task ':test'. > No tests found for given includes: (0) | 2023.03.23 |
[spring-jpa] N+1 문제 관련 jpa 돌아보기 -1 (0) | 2023.03.14 |
[spring framework 6, boot 3] http rest clients (0) | 2023.03.11 |
[springboottest] 테스트에서 h2-console 사용하기 (0) | 2023.02.23 |