[자바 ORM 표준 JPA 프로그래밍] 14장. 컬렉션과 부가 기능
by Jo
14장. 컬렉션과 부가 기능
14.1 컬렉션
- 자바에서 기본 제공하는 컬렉션 클래스들은 JPA에서 다음과 같은 경우 사용할 수 있음
@OneToMany
,@ManyToMany
사용해 일대다, 다대다 엔티티 관계 매핑 시@ElementCollection
사용해 값 타입 하나 이상 보관 시
- Collection: 자바가 제공하는 최상위 컬렉션. 하이버네이트는 중복을 허용하며 순서 보장하지 않는다고 가정
- Set: 중북을 허용하지 않는 컬렉션. 순서 보장 X
- List: 순서가 있는 컬렉션. 순서 보장, 중복 허용 X
- Map: Key, Value 구조로 되어 있는 특수한 컬렉션
14.1.1 JPA와 컬렉션
- 하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 감싸서 사용
- 하이버네이트에서 제공하는 내장 컬렉션을 래퍼 컬렉션이라고도 부름
- 인터페이스마다 사용되는 래퍼 컬렉션이 다름
// PersistentBag
@OneToMany
Collection<Member> collection = new ArrayList<Member>();
// PersistentBag
@OneToMany
List<Member> list = new ArrayList<Member>();
// PersistentSet
@OneToMany
Set<Member> set = new HashSet<Member>();
// PersistentList
@OneToMany
@OrderColumn
List<Member> orderColumnList = new ArrayList<Member>();
컬렉션 인터페이스 | 내장 컬렉션 | 중복 허용 | 순서 보관 |
---|---|---|---|
Collection, List | PersistentBag | O | X |
Set | PersistentSet | X | X |
List + @OrderColumn | PersistentList | O | O |
14.1.2 Collection, List
PersistentBag
을 래퍼 컬렉션으로 사용ArrayList
로 초기화 하면 됨- 중복을 허용하므로
add()
메소드로 엔티티 추가 시 중복 여부 비교하지 않고 저장만 함add()
메소드는 항상 true 리턴- ∴ 엔티티 추가해도 지연 로딩된 컬렉션 초기화 X
14.1.3 Set
PersistentSet
을 래퍼 컬렉션으로 사용HashSet
으로 초기화- 중복을 허용하지 않아
add()
메소드로 추가시equals()
메소드로 중복 체크- 없으면 객체 추가 후 true 리턴, 있으면 추가 안하고 false 리턴
HashSet
은 해시 알고리즘 쓰므로hashCode()
도 비교에 사용- ∴ 엔티티 추가시 지연 로딩된 컬렉션을 초기화 함
14.1.4 List + @OrderColumn
List
인터페이스에@OrderColumn
붙이면 순서 있는 컬렉션으로 인식- DB에 순서용 컬럼을 매핑해서 관리
PersistentList
사용
@OrderColumn의 단점
@Entity
public class Board {
@Id
@GeneratedValue
private Long id;
private String title;
private String content;
@OneToMany(mappedBy = "board")
// @OrderColumn 엔티티로 위치 지정해줄 필드 설정
@OrderColumn(name = "position") // 매핑은 Board 에 되어있지만 1:N 관계 특성상 N 쪽인 comment 테이블에 저장됨
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
을 Board 엔티티에서 매핑하므로 Comment 는 position 값 알 수 없음- 따라서 Comment INSERT 시에 값 저장되지 않고, Board 의 comments 값으로 comment의 position 값 UPDATE 하는 쿼리 추가로 실행됨
- List 변경 시 연관된 위치값 다 저장해야 함
- 중간거 하나 삭제하면 그 뒤에거 위치값을 다 변경해야 함
- 중간에 비어있는 위치값 있으면 List 에 null 저장됨
- ex) position = 1, 3, 4 인 경우 2번 들어갈 위치에 null이 저장됨
14.1.5 @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<Member>();
...
}
...
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "member_name")
private String username;
@ManyToOne
private Team team;
...
}
- 위와 같이
@OrderBy
어노테이션에 정렬 기준 필드와 차순 설정하면 ORDER BY 절에 해당 값 넣어서 조회- 그 순서대로 컬렉션 정렬되어 나옴
하이버네이트는
Set
에@OrderBy
적용해서 조회하면 순서 유지하려고HashSet
대신LinkedHashSet
을 사용함
14.2 @Converter
- 컨버터로 엔티티의 데이터를 변환하여 DB 저장할 수 있음
- ex) 엔티티에서 boolean 타입으로 쓰고있는 필드를 DB 저장시 0, 1 대신 Y, N 으로 저장하려면 컨버터를 사용
# 매핑할 테이블
CREATE TABLE member (
id VARCHAR(255) NOT NULL,
username VARCHAR(255),
vip VARCHAR(1) NOT NULL,
PRIMARY KEY (id)
)
// Member 엔티티
@Entity
// @Convert(converter = BooleanToYNConverter.class, attributeName = "vip") => 필드 대신 클래스 레벨에 설정해도 됨
public class Member {
@Id
private String id;
private String username;
// BooleanToYNConverter 로 boolean 값을 Y, N 문자로 변환해서 DB 저장
@Convert(converter=BooleanToYNConverter.class)
private boolean vip;
// Getter, Setter
...
}
...
// BooleanToYNConverter
@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);
}
}
- 컨버터 클래스는
@Converter
어노테이션 사용,ATtributeConverter
인터페이스 구현해야 함- 제네릭에 <현재 타입, 변환할 타입> 을 지정
// AttributeConverter
// X: 현재 타입, Y: 변환할 타입
public interface AttributeConverter<X, Y> {
public X convertToDatabaseColumn (X attribute);
public Y convertToEntityAttribute(Y dbData);
}
AttributeConverter
인터페이스에는 구현해야 할 메소드 두 개 있음convertToDatabaseColumn()
: 엔티티 데이터를 DB 컬럼 저장용 데이터로 변환convertToEntityAttribute()
DB 에서 조회한 컬럼 데이터를 엔티티 데이터로 변환
14.2.1 글로벌 설정
- 컨버터 타겟 타입에 별도 설정 없이 적용하려면 컨버터 작성 시
@Converter(autoApply = true)
옵션 사용 - 위 옵션 사용 시
@Converter
지정 안해도 자동으로 컨버터가 적용 됨
// 컨버터 글로벌 설정
@Converter(autoApply = true) // 이렇게 설정하면 모든 Boolean 타입에 대해 자동으로 컨버터 적용
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {
...
}
@Convert 속성
속성 | 기능 | 기본값 |
---|---|---|
converter | 사용할 컨버터 지정 | |
attributeName | 컨버터 적용할 필드 지정 | |
disableConversion | 글로벌 컨버터나 상속 받은 컨버터 사용 안함 | false |
14.3 리스너
- JPA 리스너 기능을 사용하면 엔티티 생명주기에 따른 이벤트 처리 가능
- 이벤트를 활용하여 등록 일자, 수정 일자, 등록자, 수정자 등에 대한 기록을 리스너 하나로 처리할 수 있음
14.3.1 이벤트 종류
- PostLoad:
- 엔티티가 영속성 컨텍스트에 조회된 직후
refresh
를 호출한 직후- 2차 캐시에 저장되어 있어도 호출됨
- PrePersist:
persist()
호출하여 엔티티를 영속성 컨텍스트에 관라하기 직전- 식별자 생성 전략 사용한 경우 엔티티에 식별자 아직 없음
- 새로운 인스턴스
merge
할 때
- 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에 삭제한 직후
14.3.2 이벤트 적용 위치
- 엔티티에 직접 적용
- 별도의 리스너 등록
- 기본 리스너 사용
엔티티에 직접 사용
@Entity
public class Duck {
@Id
@GeneratedValue
public Long id;
private String name;
@PrePersist
public void prePersist() {
System.out.println("Duck.perpersist id = " + id); // Duck.prePersist id = null
}
@PostPersist
public void postPersist() {
System.out.println("Duck.postPersist id = " + id); // Duck.postPersist id = 1
}
@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");
}
...
}
별도의 리스너 등록
@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
에 디폴트 리스너로 등록
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings ...>
<persistence-unit-metadata>
<persistence-unit-defaults>
<entity-listeners>
<entity-listener class="jpabook.jpashop.comain.test.listener.DefaultListener" />
</enetiy-listeners>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>
- 여러 리스너 등록시 이벤트 호출 순서는 다음과 같음
- default listener
- 부모 클래스 listener
- listener
- entity
더 세밀한 설정
@ExcludeDefaultListeners
: 기본 리스너 무시@ExcludeSuperclassListeners
: 상위 클래스 이벤트 리스너 무시
@Entity
@EntityListeners(DuckListener.class)
@ExcludeDefaultListeners
@ExcludeSuperclassListeners
public class Duck extends BaseEntity {
...
}
14.4. 엔티티 그래프
- 엔티티 조회시 연관된 엔티티 조회하려면
FetchType.EAGER
설정하거나 fetch join 사용 - fetch join 사용 시 함께 조회할 엔티티에 따라 유사한 JPQL이 여러개 생길 수 있음
- ex) member 와 같이 조회하는 order 조회, item 과 같이 조회하는 order 조회 …
- 모두 order 조회지만 member 또는 item 을 같이 조회하는지에 따라 fetch join 부분만 달라짐
- ex) member 와 같이 조회하는 order 조회, item 과 같이 조회하는 order 조회 …
- JPA 2.1에 추가된 엔티티 그래프 사용하면 엔티티를 조회하는 시점에 함께 조회할 연관된 엔티티 선택 가능
- JPQL은 데이터 조회 기능만 수행
- 연관된 엔티티 함께 조회 기능은 엔티티 그래프 사용
- 엔티티 조회시점에 연관된 엔티티 들을 함께 조회하는 기능
14.4.1 Named 엔티티 그래프
// Order 조회시 연관된 Member 도 함께 조회되는 엔티티 그래프 예제
@NamedEntityGraph(name = "Order.withMember", attributeNodes = {
@NamedAttributeNode("member")
})
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue
@Column(name = "order_id")
private Long id;
// 지연 로딩으로 설정되어 있지만 엔티티 그래프에서 함께 조회할 속성으로 member 선택해서
// 이 엔티티 그래프 사용시 Order 조회하면 Member 도 같이 조회됨
@JoinColumn(name = "member_id")
@ManyToOne(fetch = FetchType.LAZY, optional = false)
private Member member;
...
}
- Named 엔티티 그래프는
@NamedEntityGraph
로 정의- name: 엔티티 그래프의 이름 정의
- attributeNodes:
@NamedAttributeNode
사용해서 그 값으로 함께 조회할 속성 선택
- 여러개 정의하려면
@NamedEntityGraphs
사용
14.4.2 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);
- Named entity graph 사용 시
em.getEntityGraph(Named 엔티티 그래프 이름)
으로 정의한 엔티티 그래프 찾아오면 됨 - JPA 힌트 기능을 통해 동작하며, 힌트 키로
javax.persistence.fetchgraph
사용하고 값으로 찾아온 엔티티 그래프 사용 - 만들어진 힌트를 엔티티 조회시 파라미터로 넘기면 됨
14.4.3 subgraph
- Order -> OrderItem -> Item 까지 함께 조회하는 경우
- Item 은 Order 가 관리하는 필드 아님
- 이런 경우 subgraph 를 사용
@NamedEntityGRaph(
name = "Order.withAll",
attributeNodes = {
@NamedAttributeNode("member"),
@NamedAttributeNode(value = "orderItems", subgraph = "orderItems")
},
subgraphs = @NapedSubgraph(name = "orderItems", attributeNode = {
@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", casacade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<OrderItem>();
...
}
...
@Entiy
@Table(name = "order_item")
public class OrderItem {
@Id
@GeneratedValue
@Column(name = "order_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_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);
- Named 엔티티 그래프 Order.withAll 로 Order -> Member, Order -> OrderItem, OrderItem -> Item 객체 그래프를 함께 조회
- OrderItem -> Item 은 Order 의 객체 그래프가 아니므로
subgraphs
속성에@NamedSubgraph
어노테이션으로 값 넣어서 정의
- OrderItem -> Item 은 Order 의 객체 그래프가 아니므로
14.4.4 JPQL 에서 엔티티 그래프 사용
- 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();
14.4.5 동적 엔티티 그래프
createEntityGraph()
메소드로 엔티티 그래프를 동적으로 구성 가능public <T> EntityGraph<T> createEntityGraph(Class<T> rootType);
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);
em.createEntityGraph(Order.class)
로 Order 에 대한 엔티티 그래프를 동적으로 생성graph.addAttributeNodes("member")
로Order.member
속성을 엔티티 그래프에 추가
EntityGraph<Order> graph = em.createEntityGraph(Order.class);
graph.addAttriubteNode("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);
- 마찬가지로 엔티티 그래프 동적으로 생성한 후
graph.addSubgraph("orderItems")
로 서브 그래프 생성 orderItems.addAttributeNodes("item")
으로 서브 그래프에item
속성 포함시킴
14.4.6 엔티티 그래프 정리
- ROOT 에서 시작
- 엔티티 그래프는 항상 조회하는 엔티티의 ROOT 에서 시작
- 이미 로딩된 엔티티
- 영속성 컨텍스트에 해당 엔티티가 이미 로딩되어 있으면 엔티티 그래프 적용 안됨
- 아직 초기화 되지 않은 프록시에는 적용됨
- 영속성 컨텍스트에 해당 엔티티가 이미 로딩되어 있으면 엔티티 그래프 적용 안됨
- fetchgraph, loadgraph의 차이
javax.persistence.fetchgraph
- 엔티티 그래프에 선택한 속성만 함께 조회
javax.persistence.loadgraph
- 선택한 속성 + 글로벌 fetch 모드가 FetchType.EAGER 로 설정된 연관관계도 포함해서 함께 조회
Subscribe via RSS