JPA는 자바에서 기본으로 제공하는 Collection, List, Set, Map 컬렉션을 지원하고 다음 경우에 이 컬렉션을 사용할 수 있다. 컬렉션이 필요한 경우는 다음과 같다.
@OneToMany, @ManyToMany를 사용해서 일대다나 다대다 엔티티 관계를 매핑할때
@ElementCollection을 사용해서 값 타입을 하나 이상 보관할 때
자바 컬렉션 특징
Collection : 자바가 제공하는 최상위 컬렉션이다. 하이버네이트는 중복을 허용하고 순서를 보장하지 않는다고 가정한다.
Set : 중복을 허용하지 않는 컬렉션이다. 순서를 보장하지 않는다.
List : 순서가 있는 컬렉션이다. 순서를 보장하고 중복을 허용한다.
Map : Key, Value 구조로 되어 있는 특수한 컬렉션이다.
JPA와 컬렉션
하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 감싸서 사용한다. 하이버네이트는 컬렉션을 효율적으로 관리하기 위해 엔티티를 영속 상태로 만들 때 원본 컬렉션을 감싸고 있는 내장 컬렉션을 생성해서 이 내장 컬렉션을 사용하도록 참조를 변경한다. 하이버네이트가 제공하는 내장 컬렉션은 원본 컬렉션을 감싸고 있어서 래퍼 컬렉션으로도 부른다.
Collection, List
Collection, List는 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고 단순히 저장만 하면 된다. 따라서 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않는다.
Set
HashSet은 중복을 허용하지 않으므로 add() 메소드로 객체를 추가할 때 마다 equals() 메소드로 같은 객체가 있는지 비교한다. 같은 객체가 없으면 객체를 추가하고 true를 반환하고, 같은 객체가 이미 있어서 추가에 실패하면 false를 반환한다. 참고로 HashSet은 해시 알고리즘을 사용하므로 hashcode()도 함께 사용해서 비교한다.
Set은 엔티티를 추가할 때 중복된 엔티티가 있는지 비교해야 한다. 따라서 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화한다.
@OrderColumn을 Board 엔티티에서 매핑하므로 Comment는 POSITION의 값을 알 수 없다. 그래서 Comment를 INSERT할 때는 POSITION 값이 저장되지 않는다. POSITION은 Board.comments의 위치 값이므로, 이 값을 사용해서 POSITION의 값을 UPDATE하는 SQL이 추가로 발생한다.
List를 변경하면 연관된 많은 위치 값을 변경해야 한다. 예를 들어 2번 인덱스의 row를 삭제하면 3번, 4번 row의 POSITION값을 각각 하나씩 줄이는 UPDATE SQL이 2번 추가로 실행된다.
중간에 POSITION 값이 없으면 조회한 List에는 Null이 보관된다. 예를 들어 2번 인덱스의 row를 강제로 삭제하고 다른 row를 수정하지 않는다고 가정 하면 List를 조회할 경우 2번 위치에 null 값이 보관되어 컬렉션을 순회할 때 NullPointerException이 발생한다.
@OrderBy
@OrderColumn이 데이터베이스에 순서용 컬럼을 매핑해서 관리했다면 @OrderBy는 데이터베이스의 ORDER BY절을 사용해서 컬렉션을 정렬한다. 따라서 순서용 컬럼을 매핑하지 않아도 된다. 그리고 @OrderBy는 모든 컬렉션에 사용할 수 있다.
@EntitypublicclassTeam { @Id @GeneratedValueprivateLong id;privateString name; @OneToMany(mappedBy ="team") @OrderBy("username desc, id asc")privateSet<Member> members =newHashSet<Member>();...}
PreUpdate : flush나 commit을 호출해서 엔티티를 데이터베이스에 수정하기 직전에 호출된다.
PreRemove : remove() 메소드를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출된다. 또한 삭제 명령어로 영속성 전이가 일어날 때도 호출된다. orphanRemoval에 대해서는 flush나 commit 시에 호출된다.
PostPersist : flush나 commit을 호출해서 엔티티를 데이터베이스에 저장한 직후에 호출된다. 식별자가 항상 존재한다. 참고로 식별자 생성 전략이 IDENTITY면 식별자를 생성하기 위해 persist()를 호출하면서 데이터베이스에 해당 엔티티를 저장하므로 이때는 persist()를 호출한 직후에 바로 PostPersist가 호출된다.
PostUpdate : flush나 commit을 호출해서 엔티티를 데이터베이스에 수정한 직후에 호출된다.
PostRemove : flush나 commit을 호출해서 엔티티를 데이터베이스에 삭제한 직후에 호출된다.
@Entity@EntityListeners(DuckListener.class)publicclassDuck {...}publicclassDuckListener { @PrePersist// 특정 타입이 확실하면 특정 타입을 받을 수 있다. privatevoidperPersist(Object obj) {System.out.println("DuckListener.prePersist obj = ["+ obj +"]"); } @PostPersist// 특정 타입이 확실하면 특정 타입을 받을 수 있다. privatevoidpostPersist(Object obj) {System.out.println("DuckListener.postPersist obj = ["+ obj +"]"); }}
기본 리스너 사용
모든 엔티티의 이벤트를 처리하려면 META-INF/orm.xml에 기본 리스너로 등록하면 된다.
javax.persistence.ExcludeDefaultListeners : 기본 리스너 무시
javax.persistence.ExcludeSuperclassListeners : 상위 클래스 이벤트 리스너 무시
엔티티 그래프
글로벌 fetch 옵션은 애플리케이션 전체에 영향을 주고 변경할 수 없는 단점이 있다. 그래서 일반적으로 글로벌 fetch 옵션은 FetchType.LAZY를 사용하고, 엔티티를 조회할 때 연관된 엔티티를 함께 조회할 필요가 있으면 JPQL의 페치 조인을 사용한다.
그런데 페치 조인을 사용하면 같은 JPQL을 중복해서 작성하는 경우가 많다.
// case 1select o from Order o where o.status=?// case 2select o from Order o join fetch o.member where o.status=?// case 3select o from Order o join fetch o.orderItems where o.status=?
3가지 JPQL 모두 주문을 조회하는 JPQL이지만 함께 조회할 엔티티에 따라서 다른 JPQL을 사용해야 한다.
이는 엔티티 그래프를 사용해서 연관된 엔티티를 함께 조회하면 되고 JPQL은 데이터를 조회하는 기능만 수행할 수 있다.
Named 엔티티 그래프를 사용하려면 정의한 엔티티 그래프를 em.getEntityGraph("Order.withMember")를 통해서 찾아오면 된다. 엔티티 그래프는 JPA의 힌트 기능을 사용해서 동작하는데 힌트의 키로 javax.persistence.fetchgraph를 사용하고 힌트의 값으로 찾아온 엔티티 그래프를 사용하면 된다.
EntityGraph graph =em.getEntityGraph("Order.withMember");Map hints =newHashMap();hints.put("javax.persistence.fetchgraph", graph);Order order =em.find(Order.class, orderId, hints);
subgraph
Order → OrderItem → Item까지 함께 조회해보자. Order → OrderItem은 Order가 관리하는 필드지만 OrderItem → Item은 Order가 관리하는 필드가 아니다.
JPQL에서 엔티티 그래프를 사용하는 방법은 em.find()와 동일하게 힌트만 추가하면 된다.
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();
동적 엔티티 그래프
엔티티 그래프를 동적으로 구성하려면 createEntityGraph() 메소드를 사용하면 된다.
영속성 컨텍스트에 해당 엔티티가 이미 로딩되어 있으면 엔티티 그래프가 적용되지 않는다.(아직 초기화되지 않은 프록시에는 엔티티 그래프가 적용된다.)
Order order =em.find(Order.class, orderId); // 이미 조회hints.put("javax.persistence.fetchgraph",em.getEntityGraph("Order.withMember"));Order order2 =em.find(Order.class, orderId, hints);
fetchgraph, loadgraph의 차이
fetchgraph는 엔티티 그래프에 선택한 속성만 함께 조회한다. 반면에 loadgraph 속성은 엔티티 그래프에 선택한 속성뿐만 아니라 글로벌 fetch 모드가 FetchType.EAGER로 설정된 연관관계도 포함해서 함께 조회한다.
정리
컨버터를 사용하면 엔티티의 데이터를 변환해서 데이터베이스에 저장할 수 있다.
리스너를 사용하면 엔티티에서 발생한 이벤트를 받아서 처리할 수 있다.
페치 조인은 객체지향 쿼리를 사용해야 하지만 엔티티 그래프를 사용하면 객체지향 쿼리를 사용하지 않아도 원하는 객체 그래프를 한 번에 조회할 수 있다.