Table of Contents
컬렉션
JPA는 자바에서 기본으로 제공하는 Collection, List, Set, Map 컬렉션을 지원하고 다음의 경우에 이 컬렉션을 사용할 수 있다.
- @OneToMany, @ManyToMany를 사용해서 일대다, 다대다 엔티티 관계를 매핑할 때
- @ElementCollection을 사용해서 값 타입을 하나 이상 보관할 때
자바 컬렉션 인터페이스의 특징은 다음과 같다.
- Collection: 하이버네이트는 중복을 허용하고 순서를 보장하지 않는다고 가정
- Set: 중복을 허용하지 않는 컬렉션이다. 순서를 보장하지 않는다.
- List: 순서가 있는 컬렉션이다. 순서를 보장하고 중복을 허용한다.
- Map: Key, Value 구조로 된 특수한 컬렉션
JPA 명세에는 자바 컬렉션 인터페이스에 대한 특별한 언급이 없다.
따라서 JPA 구현체에 따라서 제공하는 기능이 조금씩 다를 수 있다.
여기서는 하이버네이트 구현체를 기준으로 이야기한다.
JPA와 컬렉션
before persist = class java.util.ArrayList
after persist = class org.hibernate.collection.internal.PersistentBag
출력 결과를 보면 원래 ArrayList 타입이었다.
영속 상태로 만든 직후 하이버네이트가 제공하는 PersistentBag 타입으로 변경되었다.
하이버네이트는 컬렉션을 효율적으로 관리하기 위해 엔티티를 영속 상태로 만들 때 원본 컬렉션을 감싸고 있는 내장 컬렉션을 생성해서 이 내장 컬렉션(래퍼 컬렉션)을 사용하도록 참조를 변경한다
하이버네이트는 이런 특징 때문에 컬렉션을 사용할 때 다음처럼 즉시 초기화해서 사용하는 것을 권장한다.
1
Collection<Member> members = new ArrayList<Member>();
하이버네이트 내장 컬렉션과 특징
컬렉션 인터페이스 | 컬렉션 | 중복 허용 | 순서 보관 | 특징 |
---|---|---|---|---|
Collection, List | PersistenceBag | O | X | 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화 X |
Set | PersistenceSet | X | X | 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화 |
List + @OrderColumn | PersistentList | O | O | 아래 확인 |
List + @OrderColumn
데이터베이스에 순서 값을 저장해서 조회할 때 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@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;
...
}
@OrderColumn의 단점
다음과 같은 단점들 때문에 실무에서 잘 사용하지 않는다.
- @OrderColumn을 Board 엔티티에서 매핑하므로 Comment는 POSITION의 값을 알 수 없다.
그래서 Comment를 INSERT할 때는 POSITION 값이 저장되지 않는다. 추가의 UPDATE SQL이 발생 - List를 변경하면 연관된 많은 위치 값을 변경해야 한다. 2번을 삭제하면, 3, 4 .. 위치를 수정해야 한다.
- 중간에 POSITION 값이 없으면 조회한 List에 null이 보관된다.
@OrderBy
데이터베이스의 ORDER BY 절을 사용해서 컬렉션을 정렬.
모든 컬렉션에서 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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<Member>();
...
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "MEMBER_NAME")
private String username;
@ManyToOne
private Team team;
...
}
@Converter
엔티티의 데이터를 변환해서 데이터베이스에 저장하거나 엔티티로 변환해서 가져올수 있다.
속성 | 기능 | 기본값 |
---|---|---|
converter | 사용할 컨버터를 지정한다 | |
attributeName | 컨버터를 적용할 필드를 지정 | |
disableConversion | 글로벌 컨버터나 상속 받은 컨버터를 사용하지 않는다. | false |
글로벌 설정
모든 Boolean 타입에 컨버터를 적용하려면 아래와 같이 적용하면 된다.
1
2
@Converter(autoApply = true)
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {}
리스너
모든 엔티티를 대상으로 언제 어떤 사용자가 삭제를 요청했는지 모두 로그로 남겨야 하는 요구사항이 있다고 가정하자
JPA 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트를 처리할 수 있다.
이벤트 종류
이벤트 적용 위치
엔티티에 직접 적용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Entity
public class Duck {
@Id @GeneratedValue
public Long id;
private String name;
@PrePersist
public void prePersist() {
System.out.println("Duck.prePersist id=" + id);
}
public void postPersist() {
System.out.println("Duck.postPersist id=" + id);
}
@PostLoad
public void postLoad() {
System.out.println("Duck.postLoad");
}
@PreRemove
public void preRemove() {
System.out.println("Duck.preRemove");
}
@PostRemove
public void postRemove() {
System.out.println("Duck.postRemove");
}
...
}
별도의 리스너 등록
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Entity
@EntityListeners(DuckListener.class)
public class Duck {...}
public class DuckListener {
@PrePersist
// 특정 타입이 확실하면 특정 타입을 받을 수 있다.
private void prePersist(Object obj) {
System.out.println("DuckListener.prePersist obj = [" + obj + "]");
}
@PostPersist
// 특정 타입이 확실하면 특정 타입을 받을 수 있다.
private void postPersist(Object obj) {
System.out.println("DuckListener.postPersist obj = [" + obj + "]");
}
}
기본 리스너 사용
모든 엔티티의 이벤트를 처리하려면 META-INF/orm.xml에 기본 리스너로 등록
여러 리스너를 등록했을 때 이벤트 호출 순서는 다음과 같다.
- 기본 리스너
- 부모 클래스 리스너
- 리스너
- 엔티티
더 세밀한 설정
- javax.persistence.ExcludeDefaultListeners: 기본 리스너 무시
- javax.persistence.ExcludeSuperclassListeners: 상위 클래스 이벤트 리스너 무시
엔티티 그래프
JPA 2.1에 추가된 엔티티 그래프 기능을 사용하면 엔티티를 조회하는 시점에 함께 조회할 연관된 엔티티를 선택할 수 있다.
엔티티 그래프 기능은 엔티티 조회시점에 연관된 엔티티들을 함께 조회하는 기능이다.
Named 엔티티 그래프
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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로 정의한다.
- name: 엔티티 그래프의 이름을 정의
- attributeNodes: 함께 조회할 속성을 선택
위의 예제는 지연로딩으로 설정했지만, 엔티티 그래프에서 함께 조회할 속성으로 설정이되어 Order를 조회할 때 연관된 member도 함께 조회
둘 이상 정의하려면 @NamedEntityGraphs를 사용하면 된다.
em.find()에서 엔티티 그래프 사용
1
2
3
4
5
6
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”)를 통해서 찾아오면 된다.
자세한 내용은 코드 참고
subgraph
Order -> OrderItem -> Item까지 함께 조회해보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@NamedEntityGraph(name = "Order.withAll", attributeNodes = {
@NamedAttributeNode("member"),
@NamedAttributeNode(value = "orderItems", subgraph = "orderItems")
},
subgraphs = @NamedSubgraph(name = "orderItems", attributeNodes = {
@NamedAttributeNode("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를 사용해서 서브 그래프를 정의한다.
JPQL에서 엔티티 그래프 사용
em.find와 동일하게 hints만 추가하면 된다.
1
2
3
4
5
6
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() 메소드를 사용하면 된다.
1
2
3
4
5
6
7
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);
subgraph 기능을 동적으로 구성
1
2
3
4
5
6
7
8
9
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에서 시작
엔티티 그래프는 항상 조회하는 엔티티의 ROOT에서 시작해야 한다.
이미 로딩된 엔티티
영속성 컨텍스트에 해당 엔티티가 이미 로딩되어 있으면 엔티티 그래프가 적용되지 않는다.
fetchgraph, loadgraph의 차이
fetchgraph는 엔티티 그래프에 선택한 속성만 함께 조회한다.
loadgraph 속성은 엔티티 그래프에 선택한 속성뿐만 아니라 글로벌 fetch 모드가 EAGER로 설정된 연관관계도 포함해서 함께 조회