14장_컬렉션과 부가 기능

컬렉션

  • @OneToMany, @ManyToMany 매핑할 때
  • @ElementCollection 으로 값 타입 하나 이상 보관할 때
  • Collection: 자바가 제공하는 최상위 컬렉션, 중복 허용하고 순서 보장하지 않는다.
  • Set: 중복을 허용하지 않는 컬렉션, 순서 보장하지 않는다.
  • List: 순서가 있는 컬렉션, 순서 보장하고 중복을 허용한다.
  • Map: Key, Value 구조로 되어 있는 특수한 컬렉션
JPA와 컬렉션

하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 감싸서 사용한다.

컬렉션 인터페이스 내장 컬렉션 중복 허용 순서 보관
Collection, List PersistenceBag O X
Set PersistenceSet X X
List + @OrderColumn PersistentList O O
Collection, List

ArrayList로 초기화 중복을 허용하기 때문에 엔티티를 추가할때 비교 없이 단순히 저장만 한다. 엔티티를 추가할 때 지연로딩된 컬렉션을 초기화 하지 않는다.

Set

HashSet으로 초기화 중복을 허용하지 않으므로 엔티티를 추가할때 비교해야 한다. 엔티티를 추가할 때 지연로딩된 컬렉션을 초기화한다.

List + @OrderColumn

순서가 있는 특수 컬렉션

@Entity
public class Board {

    @Id @GeneratedValue
    private Long id;

    private String title;
    private String content;

    @OneToMany(mappedBy = "board")
    @OrderColumn(name = "POSITION")
    private List<Comment> comments = new ArrayList<Comment>();

    ...   
}

@Entity
public class Comment {

    @Id @GeneratedValue
    private Long id;

    private String comment;

    @ManyToOne
    @JoinColumn(name = "BOARD_ID")
    private Board board;

    ...
}

list.add(1,data1); //1번 위치에 data1을 저장하라. list.get(10); //10번 위치에 있는 값을 조회하라.

장점보단 단점이 많아 실무에선 사용하지 않는다.

  • comment를 INSERT할 때 POSITION 값이 저장되지 않는다. @OrderColumn을 Board 엔티티에 매핑하기 때문에 Board.commnets 의 위치값을 사용해서 POSITION값을 UPDATE 하는 SQL이 추가로 발생한다.
  • comment를 삭제 또는 위치 변경시 연관된 많은 위치 값을 변경해야 한다.
  • 중간에 POSITION 값이 없으면 조회한 리스트에 null이 보관된다. > 컬렌션을 순회할때 NullPointerException이 발생한다.
@OrderBy

데이터베이스의 ORDER BY절을 사용해서 컬렉션을 정렬한다. 모든 컬렉션에서 사용할 수 있다.

@Converter

엔티티의 데이터를 변환해서 데이터베이스에 저장

@Entity
@Convert(converter=BooleanToYNConverter.class, attributeName = "vip")
public class Member {

    @Id
    private String id;
    private String username;

    @Convert(converter=BooleanToYNConverter.class)
    private boolean vip;

    ...
}
@Converter
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {

    @Override
    public String convertToDatabaseColumn(Boolean attribute) {
        return (attribute != null && attribute) ? "Y" : "N";
    }

    @Override
    public Boolean convertToEntityAttribute(String dbData) {
        return "Y".equals(dbData);
    }
}
public interface AttributeConverter<X,Y> {

    public Y convertToDatabaseColumn (X attribute);
    public X convertToEntityAttribute (Y dbData);
}
@Entity
@Convert(converter=BooleanToYNConverter.class, attributeName = "vip")
public class Member {

    @Id
    private String id;
    private String username;

    private boolean vip;

    ...
}
글로벌 설정
@Converter(autoApply = true)
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {

    @Override
    public String convertToDatabaseColumn(Boolean attribute) {
        return (attribute != null && attribute) ? "Y" : "N";
    }

    @Override
    public Boolean convertToEntityAttribute(String dbData) {
        return "Y".equals(dbData);
    }
}
@Entity
public class Member {

    @Id
    private String id;
    private String username;

    private boolean vip;

    ...
}

@Convert 속성 정리

속성 기능 기본값
converter 사용할 컨버터를 지정한다.
attributeName 컨버터를 적용할 필드를 지정한다.
disabledConversion 글로벌 컨버터나 상속 받은 컨버터를 사용하지 않는다. false

리스너

이벤트 종류
  1. PostLoad: 엔티티가 영속성 컨텍스트에 조회된 직후 또는 refresh를 호출한 후
  2. PrePersist: persist() 메소드를 호출해서 엔티티를 영속성 컨넥스트에 관리하기 직전에 호출된다. 식별자 생성 전략을 사용한 경우 엔티티에 식별자는 아직 존재하지 않는다. 새로운 인스턴스를 merge할 때도 수행된다.
  3. PreUpdate: flush나 commit을 호출해서 엔티티를 데이터베이스에 수정하기 직전에 호출된다.
  4. PreRemove: remove() 메소드를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출된다. 또한 삭제 명령어로 영속성 전이가 일어날 때도 호출된다. orphanRemoval에 대해서는 flush 나 commit시에 호출된다.
  5. PostPersist: flush나 commit을 호출해서 엔티티를 데이터베이스에 저장한 직후에 호출된다. 식별자가 항상 존재한다. 참고로 식별자 생성 정략이 IDENTITY면 식별자를 생성하기 위해 persist()를 호출함녀서 데이터베이스에 해당 엔티티를 저장하므로 이때는 persist()를 호출한 직후에 바로 PostPersist가 호출된다.
  6. PostUpdate: flush나 commit을 호출해서 엔티티를 데이터베이스에 수정한 직후에 호출된다.
  7. PostRemove: flush나 commit을 호출해서 엔티티를 데이터베이스에 삭제한 직후에 호출된다.
이벤트 적용 위치
  • 엔티티에 직접 적용
@Entity
public class Duck {
    @ID @GeneratedValue
    public Long id;

    private String name;

    @PrePersist
    public void prePersist() {
        System.out.println("Duck.prePersist id=" + id);
    }

    @PostPersist
    publci void postPersist() {
        System.out.println("Duck.postPersist id=" + id);
    }

    @PreLoad
    public void preLoad() {
        System.out.println("Duck.preLoad");
    }

    @PreRemove
    public void preRemove() {
        System.out.println("Duck.preRemove");
    }

    @PostRemove
    publci void postRemove() {
        System.out.println("Duck.postRemove");
    }
    ...
}
  • 별도의 리스너 등록
@Entity
@EntityListeners(DuckListener.class)
public class Duck {
    @ID @GeneratedValue
    public Long id;

    private String name;

    ...
}


public class DuckListener {

    @PrePersist
    //특정 타입이 확실하면 특정 타입을 받을 수 있다.
    public void prePersist(Object obj) {
        System.out.println("DuckListener.prePersist obj = [" + obj + "]");
    }

    @PostPersist
    //특정 타입이 확실하면 특정 타입을 받을 수 있다.
    publci void postPersist(Object obj) {
        System.out.println("DuckListener.postPersist obj = [" + obj + "]");
    }
}
  • 기본 리스너 사용
<?xml version="1.0" encoding="UTF-f"?>
<entity-mappings ...>

    <persistence-unit-metadata>
        <persistence-unit-defaults>
            <entity-listeners>
                <entity-listener class="jpabook.jpashop.domain.test.lister.DefaultListener" />
            </entity-listeners>
        </persistence-unit-defaults>
    </persistence-unit-metadata>

</entity-mappings>

여러 리스너를 등록했을 때 이벤트 호출순서

  1. 기본 리스너
  2. 부모 클레스 리스너
  3. 리스너
  4. 엔티티
더 세밀한 설정
  • javax.persistence.ExcludeDefaultListeners: 기본 리스너 무시
  • javax.persistence.ExcludeSuperclassListeners: 상위 클래스 이벤트 리스너 무시
@Entity
@EntityListeners(DuckListener.class)
@ExcludeDefaultListeners
@ExcludeSuperclassListeners
public class Duck extends BaseEntity {
    ...
}

대부분의 엔티티에 공통으로 적용하는 등록일자, 수정일자, 등록자, 수정자 처리 및 기록을 리스너 하나로 처리할 수 있다.

엔티티 그래프

Named 엔티티 그래프
@NamedEntityGraph(name = "Order.withMember", attributeNodes = {@NamedAttributeNode("member")})
@Entity
@Table(name = "ORDERS")
public class Order {

    @ID @GeneratedValue
    @Column(name = "ORDER_ID")
    public Long id;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "MEMBER_ID")
    private Member member;    //주문 회원

    ...
}
em.find()에서 엔티티 그래프 사용
EntityGraph graph = em.getEntityGraph("Order.withMember");

Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);

Order order = em.find(Order.class, orderId, hints);
select o.*, m.*
from
    ORDERS o
inner join
    Member m
        on o.MEMBER_ID = m.MEMBER_ID
where
    o.ORDER_ID=?
subgraph

Order -> OrderItem -> Item 까지 함께 조회


@NamedEntityGraph(name = "Order.withAll", attributeNodes = {
    @NamedAttributeNode("member"),
    @NamedAttributeNode(value = "orderItems", subgraph = "orderItems")
    },
    subgraphs = @NamedSubgraph(anme = "orderItems", attributeNodes = {
        @NamedAttributeNode("item")
    })
)
@Entity
@Table(name = "ORDERS")
public class Order {

    @ID @GeneratedValue
    @Column(name = "ORDER_ID")
    public Long id;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "MEMBER_ID")
    private Member member;    //주문 회원

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<OrderItem>();

    ...
}

@Entity
@Table(name = "ORDER_ITEM_ID")
public class OrderItem {

    @ID @GeneratedValue
    @Column(name = "ORDER_ITEM_ID")
    public Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ITEM_ID")
    private Item item;    //주문 상품

    ...
}
Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", em.getEntityGraph("Order.withAll"));

Order order = em.find(Order.class, orderId, hints);
select o.*, m.*, oi.*, i.*
from
    ORDERS o
inner join
    Member m
        on o.MEMBER_ID = m.MEMBER_ID
left outer join
    ORDER_ITEM oi
        on o.ORDER_ID = oi.ORDER_ID
left outer join
    Item i
        on oi.ITEM_ID = i.ITEM_ID
where
    o.ORDER_ID=?
JPQL에서 엔티티 그래프 사용
List<Order> resultList = 
    em.createQuery("select o from Order o where o.id = :orderId",Order.class)
        .setParameter("orderId", orderId)
        .setHint("javax.persistence.fetchgraph", em.getEntityGraph("Order.withAll"))
        .getResultList();
동적 엔티티 그래프
EntityGraph<Order> graph = em.createEntityGraph(Order.class);
graph.addAttributeNodes("member");

Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);

Order order = em.find(Order.class, orderId, hints);
EntityGraph<Order> graph = em.createEntityGraph(Order.class);
graph.addAttributeNodes("member");
Subgraph<OrderItem> orderItems = graph.addSubgraph("orderItems");
orderItems.addAttributeNodes("item");

Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);

Order order = em.find(Order.class, orderId, hints);
엔티티 그래프 정리
  • ROOT에서 시작
  • 이미 로딩된 엔티티
    Order order1 = em.find(Order.class, orderId); //이미 조회
    hints.put("javax.persistence.fetchgraph", em.getEntityGraph("Order.withMember"));
    Order order2 = en.find(Order.class, orderId, hints);
    
    위의 경우 order2에 엔티티 그래프 적용 되지 않고 order1과 같은 인스턴스가 반환된다.

정리

  • JPA가 지원하는 컬렉션의 종류와 특징
  • 컨버터를 사용하면 엔티티의 데이터를 변환해서 데이터베이스에 저장할 수 있다.
  • 리스너를 사용하면 엔티티에서 발생한 이벤트를 받아서 처리할 수 있다.
  • 페치 조인은 객체지향 쿼리를 사용해야 하지만 엔티티 그래프를 사용하면 객체지향 쿼리를 사용하지 않아도 원하는 객체 그래프를 한 번에 조회할 수 있다.

results matching ""

    No results matching ""