JPA 컬렉션과 부가 기능
컬렉션
JPA는 자바에서 기본으로 제공하는 Collection, List, Set, Map 컬렉션을 지원한다. 그리고 다음과 같은 상황에 컬렉션을 사용한다.
@OneToMany
,@ManyToMany
를 사용해서 일대다, 다대다 엔티티 관계를 매핑할 때@ElementCollection
을 사용해서 값 타입을 하나 이상 보관할 때
JPA와 컬렉션
하이버네이트는 컬렉션을 효율적으로 관리하기 위해 엔티티를 영속 상태로 만들 때 원본 컬렉션을 감싸고 있는 내장 컬렉션을 생성하여 이 내장 컬렉션을 사용하도록 참조를 변경한다. 이 때 하이버네이트가 제공하는 내장 컬렉션을 래퍼 컬렉션이라고도 부른다.
하이버네이트는 이런 특징 때문에 컬렉션을 사용할 때 즉시 초기화를 권장한다.
// org.hibernate.collection.internal.PersistentBag
@OneToMany
Collection<Member> collection = new ArrayList<>();
// org.hibernate.collection.internal.PersistentBag
@OneToMany
List<Member> list = new ArrayList<>();
// org.hibernate.collection.internal.PersistentSet
@OneToMany
Set<Member> set = new HashSet<>();
// org.hibernate.collection.internal.PersistentList
@OneToMany
@OrderColumn
List<Member> orderColumnList = new ArrayList<>();
컬렉션 인터페이스 | 하이버네이트 내장 컬렉션 | 중복 | 순서 |
---|---|---|---|
Collection , List |
org.hibernate.collection.internal.PersistentBag |
O | X |
Set |
org.hibernate.collection.internal.PersistentSet |
X | X |
List + @OrderColumn |
org.hibernate.collection.internal.PersistentList |
O | O |
Collection, List
Collection, List 는 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고 단순히 저장만 하면 된다. 따라서 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않는다.
Set
Set 은 엔티티를 추가할 때 중복된 엔티티가 있는지 비교해야 한다. 따라서 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화한다.
List + @OrderColumn
순서가 있는 컬렉션은 DB에 순서 값을 저장해서 조회할 때 사용한다.
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "board")
@OrderColumn(name = "POSITION")
private List<Comment> comments = new ArrayList<Comment>();
...
}
@OrderColumn의 단점
-
위 코드에서
@OrderColumn
을 Board 엔티티에서 매핑하므로 Comment는 POSITION의 값을 알 수 없다. 그래서 Comment를 INSERT 할 때는 POSITION 값이 저장되지 않는다. POSITION은Board.comments
의 위치값이므로, 이 값을 사용해서 POSITION의 값을 UPDATE하는 SQL이 추가로 발생한다. -
List를 변경하면 연관된 많은 위치 값을 변경해야 한다. 예를 들어 2번 인덱스의 row를 삭제하면 3번, 4번 row의 POSITION 값을 하나씩 줄이는 UPDATE SQL이 추가로 실행된다.
-
중간에 POSITION 값이 없으면 조회한 List에는 null이 보관된다. 예를 들어 2번 인덱스의 row를 강제로 삭제하고 다른 row를 수정하지 않는다고 가정하면 List를 조회할 경우 2번 위치에 null 값이 보관되어 컬렉션을 순회할 때
NullPointerException
이 발생한다.
@OrderBy
@OrderColumn
이 DB에 순서용 컬럼을 매핑해서 관리한다면 @OrderBy
는 DB의 ORDER BY절을 사용해서 컬렉션을 정렬한다. 따라서 순서용 컬럼을 매핑하지 않아도 된다. 그리고 모든 컬렉션에 사용할 수 있다.
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
@OrderBy("username desc, id asc")
private Set<Member> members = new HashSet<>();
...
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "MEMBER_NAME")
private String username;
@ManyToOne
private Team team;
...
}
Team team = em.find(Team.class, team.getId());
team.getMembers(); // 초기화
// SQL (Team.member 를 초기화할 때 발생)
SELECT M.*
FROM MEMBER M
WHERE M.TEAM_ID = ?
ORDER BY
M.MEMBER_NAME DESC,
M.ID DESC
@Converter
컨버터를 사용하면 엔티티의 데이터를 변환해서 DB에 저장할 수 있다.
@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 convertToDatebaseColumn(Boolean attribute) {
return (attribute != null && attribute) ? "Y" : "N";
}
@Override
public Boolean convertToEntityAttribute(String dbData) {
return "Y".equals(dbData);
}
}
-
컨버터 클래스는
@Converter
애너테이션을 사용하고AttributeConverter
인터페이스를 구현해야 한다. -
convertToDatebaseColumn()
: 엔티티 데이터를 DB 컬럼에 저장할 데이터로 변환 -
convertToEntityAttribute()
: DB에서 조회한 컬럼 데이터를 엔티티의 데이터로 변환 -
모든 Boolean 타입에 컨버터를 적용하려면
@Converter(autoApply=true)
옵션을 적용하면 된다.
리스너
JPA 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트를 처리할 수 있다.
이벤트 종류
-
PreLoad : 엔티티가 영속성 컨텍스트에 조회된 직후 또는 refresh를 호출한 후(2차 캐시에 저장되어 있어도 호출된다.)
-
PrePersist :
persist()
를 호출하여 엔티티를 영속성 컨텍스트에 관리하기 직전에 호출된다. -
PreUpdate : flush나 commit을 호출하여 엔티티를 DB에 수정하기 직전에 호출된다.
-
PreRemove :
remove()
메서드를 호출하여 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출된다. 또한 삭제 명령어로 영속성 전이가 일어날 때도 호출된다. orphanRemoval에 대해서는 flush나 commit 시에 호출된다. -
PostPersist : flush나 commit을 호출하여 엔티티를 DB에 저장한 직후에 호출된다. 식별자가 항상 존재한다. 참고로 식별자 생성 전략이
IDENTITY
면 식별자를 생성하기 위해persist()
를 호출하면서 DB에 해당 엔티티를 저장하므로 이 때는persist()
를 호출한 직후에 바로 PostPersist가 호출된다. -
PostUpdate : flush나 commit을 호출해서 엔티티를 DB에 수정한 직후에 호출된다.
-
PostRemove : flush나 commit을 호출해서 엔티티를 DB에 삭제한 직후에 호출된다.
이벤트 적용 위치
이벤트는 엔티티에서 직접 받거나 별도의 리스너를 등록해서 받을 수 있다.
엔티티에 직접 적용
@Entity
public class Duck {
@Id @GeneratedValue
public Long id;
private String name;
@PrePersist
public void prePersist {
...
}
...
}
별도의 리스너 등록
@Entity
@EntityListeners(DuckListener.class)
public class Duck {
...
}
public class DuckListener {
@PrePersist
// 특정 타입이 확실하면 특정 타입을 받을 수 있다.
private void prePersist(Object obj) {
...
}
...
}
기본 리스너 사용
모든 엔티티의 이벤트를 처리하려면 META-INF/orm.xml
에 기본 리스너로 등록하면 된다.
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings ...>
<persistence-unit-metadata>
<persistence-unit-defaults>
<entity-listeners>
<entity-listener class="com.study.listener.DefaultListener" />
</entity-listeners>
</persistence-unit-defaults>
<persistence-unit-metadata>
</entity-mappings>
여러 리스너를 등록했을 경우 기본 리스너 -> 부모 클래스 리스너 -> 리스너 -> 엔티티 순으로 이벤트가 호출된다.
세밀한 설정
javax.persistence.ExcludeDefaultListeners
: 기본 리스너 무시javax.persistence.ExcludeSuperclassListeners
: 상위 클래스 이벤트 리스너 무시
@Entity
@EntityListeners(DuckListener.class)
@ExcludeDefaultListeners
@ExcludeSuperclassListeners
public class Duck extends BaseEntity {
...
}
엔티티 그래프
엔티티 그래프는 엔티티 조회시점에 연관된 엔티티들을 함께 조회하는 기능이다.
엔티티 조회시 연관된 엔티티를 함께 조회하려면 글로벌 페치 전략을 FetchType.EAGER
로 설정하거나 JPQL에서 페치 조인을 사용한다.
글로벌 페치 전략은 애플리케이션 전체에 영향을 주고 변경할 수 없는 단점이 있어서 일반적으로 FetchType.LAZY
를 사용하고, 연관된 엔티티를 함께 조회할 필요가 있으면 JPQL의 페치 조인을 사용한다. 그러나 페치 조인을 사용하면 같은 JPQL을 중복해서 작성하는 경우가 많다. 이 때 엔티티 그래프를 사용하면 연관된 엔티티를 함께 조회하고 JPQL은 데이터를 조회하는 기능만 수행할 수 있다.
Named 엔티티 그래프
@NamedEntityGraph(name = "Order.withMember", attributeNodes = {@NamedAttributeNode("member")})
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "MEMBER_ID")
private Member member;
...
}
@NamedEntityGraph
속성 중 attributeNodes
는 함께 조회할 엔티티를 선택한다.
EntityGraph graph = em.getEntityGraph("Order.withMember");
Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);
Order order = em.find(Order.class, orderId, hints);
Named 엔티티 그래프를 사용하려면 정의한 엔티티 그래프를 em.getEntityGraph("Order.withMember")
를 통해 찾아오면 된다. 엔티티 그래프는 JPA의 힌트 기능을 사용해서 동작하는데 힌트의 키로 javax.persistance.fetchgraph
를 사용하고 힌트의 값으로 찾아온 엔티티 그래프를 사용하면 된다.
subgraph
@NameEntityGraph(name = "Order.withAll", attributeNodes = {
@NameAttributeNode("member"),
@NameAttributeNode(value = "orderItems", subgraph = "orderItems")
},
subgraphs = @NamedSubgraph(name = "orderItems", attributeNodes = {
@NameAttributeNode("item")
})
)
@Entity
@Table(name ="ORDERS")
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private 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")
public class OrderItem {
@Id @GeneratedValue
@Column(name = "ORDER_ITEM_ID")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ITEM_ID")
private Item item;
...
}
OrderItem -> Item은 Order의 객체 그래프가 아니므로 subgraphs 속성 정의가 필요하다. @NamedSubgraph
애너테이션을 사용하여 서브 그래프를 정의한다.
Order -> OrderItem -> Item
까지 함께 조회된다.
Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", em.getEntityGraph("order.withAll"));
Order order = em.find(Order.class, orderId, hints);
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();
em.find()
에서는 필수 관계(optional=false
)를 고려해서 내부 조인을 사용하지만 JPQL에서 엔티티 그래프를 사용할 때는 항상 SQL 외부 조인을 사용한다. 그래서 SQL 내부 조인을 사용하려면 내부 조인을 명시해줘야 한다.
select o from Order o join fetch o.member where o.id = :orderId
동적 엔티티 그래프
엔티티 그래프를 동적으로 구성하려면 createEntityGraph()
메서드를 사용한다.
EntityGraph<Order> graph = em.createEntityGraph(Order.class);
graph.addAttributeNodes("member");
Subgraph<OrderItem> orderItems = graph.addSubgraph("orderItems");
orderItems.addAttributeNodes("item");
Map hints = new HashMap()
hints.get("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 = em.find(Order.class, orderId, hints); // 엔티티 그래프 적용 안됨
fetchgraph, loadgraph의 차이
fetchgraph
는 엔티티 그래프에 선택한 속성만 함께 조회한다.loadgraph
는 엔티티 그래프에 선택한 속성뿐만 아니라FetchType.EAGER
로 설정된 연관관계도 함께 조회한다.
Comments