컬렉션과 부가 기능
컬렉션
JPA는 자바에서 기본으로 제공하는 Collection
, List
, Set
, Map
컬렉션을 지원한다.
필요한 경우는 아래와 같다.
@OneToMany
,@ManyToMany
를 사용해서 일대다, 다대다 엔티티 관계를 매핑할 때 사용@ElementCollection
을 사용해서 값 타입을 하나 이상 보관할 때 사용
자바 컬렉션 구조
JPA와 컬렉션
@Entity
public class Team {
@Id
private String id;
@OneToMany
@JoinColumn
private Collection<Member> members = new ArrayList<>();
하이버네이트는 엔티티를 영속 상태로 만들 때, 컬렉션 필드를 하이버네이트에서 준비한 컬렉션을 감싸서 사용한다.
위 members
의 타입은 ArrayList
지만, 영속 상태가 되면 PersistentBag 타입으로 한 단계 감싼 래퍼 컬렉션이 된다.
Collection, List
List
는 중복을 허용하므로 객체를 추가할 때 내부적으로 어떤 비교도 하지 않고,true
를 반환한다.엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않는다.
Set
Set
은 중복을 허용하지 않는 컬렉션이다.Set
은 객체를 추가할 때마다equals()
를 통해 매번 같은 객체가 있는지 확인한다. 만약HashSet
을 사용 중이면Hash
코드를 추가로 비교한다.엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화한다.
List + @OrderColumn
List
인터페이스에@OrderColumn
을 추가하면 순서가 있는 특수한 컬렉션으로 인식한다.순서가 있다는 의미는 데이터베이스에 순서 값을 저장해서 조회한다는 뜻이다.
@OneToMany(mappedBy = "board")
@OrderColumn(name = "POSITION")
private List<Comment> comments = new ArrayList<>();
위 코드는 Board
와 Comment
가 일대다 관계이고, 테이블의 일대다 특성상 위치 값은 N
쪽에 저장해야하므로 POSITION
컬럼은 Comment
테이블에 저장된다.
@OrderColumn
의 단점
@OrderColumn
을Board
엔티티에서 매핑하므로Commnet
는POSITION
값을 알 수 없다. 그래서Comment
를 삽일할 때POSITION
값이 저장되지 않으므로,Board.comments
의 위치 값을 활용하여POSITION
을 업데이트하는 쿼리가 추가로 날아간다.List
를 변경하면 연관된 수많은 위치 값을 변경해야 한다.중간에
POSITION
이 없으면 조회한List
에는Null
이 보관된다.
@OrderBy
데이터베이스의
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>();
...
}
SELECT M.*
FROM
MEMBER M
WHERE
M.TEAM_ID=?
ORDER BY
M.MEMBER_NAME DESC,
M.ID ASC
하이버네이트는 Set
에 @OrderBy
를 적용하여 결과를 조회하면 순서를 유지하기 위해 HashSet
대신 LinkedHashSet
을 내부에서 사용한다.
@Converter
엔티티의 데이터를 변환해서 데이터베이스에 저장할 수 있다.
ex) Boolean 타입을 데이터베이스에 저장할 때 Y 또는 N으로 저장하고 싶은 경우
@Entity
public class Member {
@Id
private String id;
private String username;
@Convert(converter=BooleanToYNConverter.class)
private boolean vip;
}
@Converter(autoApply = true) // 모든 엔티티에 컨버터가 적용된다. 디폴트는 false
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
인터페이스를 구현해야 한다.
public interface AttributeConverter<X,Y> {
public Y convertToDatabaseColumn (X attribute);
public X convertToEntityAttribute (Y dbData);
}
convertToDatabaseColumn()
: 엔티티의 데이터를 데이터베이스 컬럼에 저장할 데이터로 변환한다.convertToEntityAttribute()
: 데이터베이스에서 조회한 컬럼 데이터를 엔티티의 데이터로 변환한다.
컨버터는 클래스 레벨에도 설정이 가능하다.
@Entity
@Convert(converter=BooleanToYNConverter.class, attributeName = "vip")
public class Member {
@Id
private String id;
private boolean vip;
}
글로벌 설정
모든 Boolean
타입에 컨버터를 적용하려면 @Converter(autoApply = true)
옵션을 적용하면 된다.
@Converter(autoApply = true)
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);
}
}
리스너
JPA 리스너 기능을 사용하면 엔티티의 생명 주기에 따른 이벤트를 처리할 수 있다.
예시로 모든 엔티티를 대상으로 언제 어떤 사용자가 삭제를 요청했는지 모두 로그로 남기고 싶은 경우, 이때 애플리케이션 삭제 로직을 하나씩 찾아서 남기는 것은 비효율적이다.
이벤트 종류
PreLoad
엔티티가 영속성 컨텍스트에 조회된 직후 또는 refresh를 호출한 후(2차 캐시에 저장되어 있어도 호출)
PrePersist
persist()
를 호출하여 엔티티를 영속성 컨텍스트에 관리하기 직전에 호출
PreUpdate
flush나 commit을 호출하여 엔티티를 데이터베이스에 수정하기 직전에 호출
PreRemove
remove()
를 호출하여 엔티티를 영속성 컨텍스트에서 삭제하기 직전orphanRemoval
에 대해flush
나commit
시에 호출
PostPersist
flush
나commit
을 호출하여 엔티티를 데이터베이스에 저장한 직후에 호출식별자가 항상 존재
참고로 식별자 생성 전략이
IDENTITY
면 식별자를 생성하기 위해persist()
를 호출하면서 데이터베이스에 해당 엔티티를 저장하므로 이때는persist()
호출 직후에PostPersist
가 호출된다.
PostUpdate
flush
나commit
을 호출하여 엔티티를 데이터베이스에 수정한 직후에 호출
PostRemove
flush
나commit
을 호출하여 엔티티를 데이터베이스에 삭제한 직후에 호출
이벤트 적용 위치
엔티티에 직접 적용
@Entity
public class Duck {
@Id @GeneratedValue
private Long id;
private String name;
@PrePersist
public void prePersist() {
System.out.println("Duck.prePersist id=" + id);
}
@PostPersist
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");
}
}
별도의 리스너 등록
@Entity
@EntityListeners(DuckListener.class)
public class Duck {
...
}
public class DuckListener {
@PrePersist
// 특정 타입이 확실하면 특정 타입을 받을 수 있다.
private void perPersist(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.domain.
test.listener.DefaultListener" />
</entity-listeners>
</persistence-unit-defaults>
<persistence-unit-metadata>
</entity-mappings>
더 세밀한 설정
javax.persistence.ExcludeDefaultListeners
: 기본 리스너 무시javax.persistence.ExcludeSuperclassListeners
: 상위 클래스 이벤트 리스너 무시
엔티티 그래프
엔티티를 조회할 때 연관된 엔티티를 함께 조회하려면
FetchType.EAGER
로 설정하면 되지만, 사용하지 않는 연관 관계까지 로딩하므로 효율적이지 않다.JPQL에서 페치 조인을 사용할 수 있지만, 같은 JPQL을 중복해서 작성하는 경우가 많다.
주문만 조회
주문과 회원을 함께 조회
주문과 주문 상품을 함께 조회
엔티티 그래프 기능을 사용하면 엔티티를 조회하는 시점에 함께 조회할 연관된 엔티티를 선택할 수 있다.
따라서 JPQL은 데이터를 조회하는 기능만 수행하면 되고, 연관된 엔티티를 함께 조회하는 기능은 엔티티 그래프를 사용하면 된다.
Named 엔티티 그래프
주문을 조회할 때 연관된 회원을 같이 조회한다.
@NamedEntityGraph(name = "Order.withMember", attributeNodes = {
@NamedAttributeNode("member")
})
@Entity
public class Order {
@Id
@GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "MEMBER_ID")
private Member member;
}
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);
엔티티 그래프는 JPA의 힌트 기능을 사용하기때문에 힌트의 키와 값을 같이 넣어 주어야 한다.
subgraph
Order → OrderItem → Item 과 같이 연달아서 엔티티 그래프를 조회할 경우 사용한다.
@NamedEntityGraph(name = "Order.withAll", attributeNodes = {
@NamedAttributeNode("member"),
@NamedAttributeNode(value = "orderItems", subgraph = "orderItems")
},
subgraphs = @NamedSubgraph(name = "orderItems", attributeNodes = {
@NamedAttributeNode("item")
})
)
@Entity
public class Order {
...
}
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();
힌트만 설정해주면 된다.
동적 엔티티 그래프
엔티티 그래프를 동적으로 구성하려면 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.put("javax.persistence.fetchgraph", graph);
Order order = em.find(Order.class, orderId, hints);
엔티티 그래프 정리
이미 로딩된 엔티티
영속성 컨텍스트에 해당 엔티티가 이미 로딩되어 있는 경우 엔티티 그래프가 적용되지 않는다.(아직 초기화 되지 않은 프록시에는 엔티티 그래프가 적용됨)
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 모드가 Fetch.Type.EAGER 로 설정된 연관관계도 포함해서 함께 조회한다.
참고 자료
Last updated