스터디/JPA 스터디
[JPA] 컬렉션과 부가 기능
jpa-study에서 스터디를 진행하고 있습니다.
컬렉션
- JPA는 자바에서 제공하는 Collection, List, Set, Map 컬렉션을 지원한다.
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을 사용 중이라면 해시 코드를 추가로 비교한다. - 따라서 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화한다.
List + @OrderColumn
- List 인터페이스에 @OrderColumn을 추가하면 순서가 있는 특수한 컬렉션으로 인식한다.
- 순서가 있다는 의미는 데이터베이스에 순서 값을 저장해서 조회한다는 뜻이다.
@OneToMany(mappedBy = "board")
@OrderColumn(name = "POSITION")
private List<Comment> comments = new ArrayList<>();
- 위 코드는 Board와 Comment가 일대다 관계이고, 테이블의 일대다 특성상 위치 값은 N쪽에 저장해야하므로 POSITION 컬럼은 Comment 테이블에 저장된다.
@OrderColumn의 단점
- @OrderColumn을 Board 엔티티에서 매핑하므로 Comment는 POSITION 값을 알 수 없다. 그래서 Comment를 삽입할 때 POSITION 값이 저장되지 않으므로, Board.comments의 위치 값을 활용하여 POSITION을 업데이트하는 쿼리가 추가로 날아간다.
- List를 변경하면 연관된 수많은 위치 값을 변경해야 한다.
- 중간에 POSITION이 없으면 조회한 List에는 Null이 보관된다.
@OrderBy
- 데이터베이스의 ORDER BY절을 사용해서 컬렉션을 정렬한다.
@OneToMany(mappedBy = "board")
@OrderBy("id desc, username asc")
private List<Comment> comments = new ArrayList<>();
@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);
}
}
리스너
- 모든 엔티티를 대상으로 언제 어떤 사용자가 삭제를 요청했는지 모두 로그로 남기고 싶다.
- 이때 애플리케이션 삭제 로직을 하나씩 찾아서 로그를 남기는 것은 너무 비효율적이다.
- JPA 리스너 기능을 사용하면 엔티티의 생명 주기에 따른 이벤트를 처리할 수 있다.
이벤트 종류
- PreLoad
- 엔티티가 영속성 컨텍스트에 조회된 직후 또는 refresh를 호출한 후
- PrePersist
persist()
를 호출하여 엔티티를 영속성 컨텍스트에 관리하기 직전
- PreUpdate
- flush나 commit을 호출하여 엔티티를 데이터베이스에 수정하기 직전
- PreRemove
remove()
를 호출하여 엔티티를 영속성 컨텍스트에서 삭제하기 직전- orphanRemoval에 대해 flush나 commit이 호출된 후
- PostPersist
- flush나 commit을 호출하여 엔티티를 데이터베이스에 저장한 직후
- PostUpdate
- flush나 commit을 호출하여 엔티티를 데이터베이스에 수정한 직후
- PostRemove
- flush나 commit을 호출하여 엔티티를 데이버테이스에 삭제한 직후
이벤트 적용 위치
엔티티에 직접 적용
@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) {
...
}
// 위와 같이 나머지 이벤트 오버라이딩
}
엔티티 그래프
- 엔티티를 조회할 때 연관된 엔티티를 함께 조회하려면 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()
메소드를 사용하여 동적으로addSubgraph()
를 넣어주면 된다.
출처
김영한 - 자바 ORM 표준 JPA 프로그래밍
'스터디 > JPA 스터디' 카테고리의 다른 글
[JPA] 트랜잭션과 락, 2차 캐시 (0) | 2022.03.29 |
---|---|
[JPA] 고급 주제와 성능 최적화 (0) | 2022.03.22 |
[JPA] 웹 애플리케이션과 영속성 관리 (0) | 2022.02.09 |
[JPA] 스프링 데이터 JPA (0) | 2022.02.05 |
[JPA] QueryDSL, 네이티브 SQL, 객체지향 쿼리 심화 (1) | 2022.01.30 |
댓글