반응형

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
  • 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
반응형

+ Recent posts