JPA와 컬렉션
하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 감싸서 허용한다.
@Entity
public class Team {
@Id
private String id;
@OneToMany
@JoinColumn
private Collection<Member> memberList = new ArrayList<>();
}
Team team = new Team();
System.out.println("before persist = " + team.getMemberList().getClass());
entityManager.persist(team);
System.out.println("after persist = " + team.getMemberList().getClass());
출력 결과
before persist = class java util.ArrayList
after persist = class org.hibernate.collection.internal.PersistentBag
//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는 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고 단순히 저장만 하면 된다.
따라서 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않는다. - Set은 엔티티를 추가할 때 중복된 엔티티가 있는지 비교해야 한다.
따라서 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화한다.
List + @OrderColumn
List 인터페이스에 @OrderColumn을 추가하면 순서가 있는 특수한 컬렉션으로 인식한다.
순서가 있다는 의미는 데이터베이스에 순서 값을 저장해서 조회할 때 사용한다는 의미이다.
@Entity
public class Team {
@Id
private String id;
@OneToMany
@OrderColumn(name = "position")
private List<Member> memberList = new ArrayList<>();
}
순서가 있는 컬렉션은 데이터베이스에 순서 값도 함께 관리한다.
테이블의 일대다 관계의 특성상 위치 값은 다(N) 쪽에 저장해야한다.
단점
- @OrderColumn을 Team 엔티티에서 매핑하므로 Member는 position 값을 알 수 없다.
Member를 insert 할 때는 position 값이 저장되지 않는다.
Member는 Team.memberList의 위치 값이므로, 이 값을 사용해서 position의 값을 update하는 SQL이 추가로 발생한다. - List를 변경하면 연관된 많은 위치 값을 변경해야 한다.
예를들어 중간에 값을 하나 삭제하면 position이 하나씩 밀린다. -> update SQL 추가 발생 - 중간에 position 값이 없으면 조회한 List에는 null이 보관된다.
@OrderBy
데이터베이스의 Order By절을 사용해서 컬렉션을 정리한다. -> 순서용 컬럼을 매핑하지 않아도 됨
모든 컬렉션에 사용할 수 있다.
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany
@OrderBy("username desc, id asc")
private Set<Member> memberSet = new HashSet<>();
}
@OrderBy의 값은 JPQL의 order by 처럼 엔티티의 필드 값으로 한다.
@Convertor
@Convertor를 사용하면 엔티티의 데이터를 변환해서 데이터베이스에 저장할 수 있다.
@Entity
//다음과 같이 클래스 레벨에도 설정할 수 있다.
//@Convert(converter=BooleanToConverter.class, attributeName = "vip")
public class Member {
@Id
private String id;
private String name;
@Convert(converter=BooleanToConverter.class)
private boolean vip;
}
@Converter
public class BooleanToConverter 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);
}
}
AttributeConverter 인터페이스
- convertToDatabaseColumn() : 엔티티의 데이터를 데이터베이스 컬럼에 저장할 데이터로 변환한다.
- convertToEntityAttribute() : 데이터베이스에서 조회한 컬럼 데이터를 엔티티의 데이터로 변환한다.
모든 타입에 컨버터를 적용하려면 @Converter(autoApply = true) 옵션을 적용하면 된다.
@Converter(autoApply = true)
public class BooleanToConverter 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 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트를 처리할 수 있다.
- PostLoad : 엔티티가 영속성 컨텍스트에 조회된 직후 또는 refresh를 호출한 후(2차 캐시에 저장되어있어도 호출)
- PrePersist : persist() 메소드를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에 호출 / 새로운 인스턴스를 merge할 때도 수행
- PreUpdate : flush나 commit을 호출해서 엔티티를 데이터베이스에 수정하기 직전에 호출
- PreRemove: remove() 메소드를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출 / 삭제 명령어로 영속성 전이가 일어날 때도 호출
- PostPersist : flush나 commit츨 호출해서 엔티티를 데이터베이스에 저장한 직후에 호출
- 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 id : " + id);
}
@PreRemove
public void preRemove() {
System.out.println("Duck.preRemove id : " + id);
}
@PostRemove
public void postRemove() {
System.out.println("Duck.postRemove id : " + id);
}
}
엔티티에 이벤트가 발생할 때마다 어노테이션으로 지정한 메소드가 실행된다.
엔티티 그래프
엔티티 조회시점에 연관된 엔티티들을 함께 조회하는 기능
엔티티 그래프 - Named 엔티티 그래프
주문(Order)를 조회할 때 연관된 회원(Member)도 함께 조회하는 엔티티 그래프
@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;
}
- name : 엔티티 그래프의 이름 지정
- attributeNodes : 함께 조회할 속성 선택
Member 엔티티가 지연 로딩으로 되어있지만 엔티티 그래프를 설정하였기 때문에 연관된 Member도 함께 조회할 수 있다.
엔티티 그래프 - EntityManager.find()에서 엔티티 그래프 사용
EntityGraph entityGraph = entityManager.getEntityGraph("Order.withMember");
Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", entityGraph);
Order order = entityManager.find(Order.class, orderId, hints);
정의한 엔티티 그래프를 entityManager.getEntityGraph()를 통해 찾아온다.
엔티티 그래프는 JPA의 힌트 기능을 사용해서 동작한다.
엔티티 그래프 - SubGraph
Order -> OrderItem -> Item 을 조회해본다고 가정해보자.
Order -> OrderItem은 Order가 관리하는 필드지만 OrderItem -> Item은 Order가 관리하는 필드가 아니다.
SubGraph를 사용해서 해결할 수 있다.
@NamedEntityGraph(name = "Order.withAll", attributeNodes = {
@NamedAttributeNode("member"),
@NamedAttributeNode(value = "orderItemList", subgraph = "orderItemList")},
subgraphs = @NamedSubgraph(name = "orderItemList", attributeNodes = {
@NamedAttributeNode("item")
})
)
@Entity
public class Order {
@Id @GeneratedValue
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> orderItemList = new ArrayList<>();
}
위의 코드와 같이 subGraph를 정의해서 사용할 수 있다.
엔티티 그래프 - JPQL에서 엔티티 그래프 사용
JPQL에서 엔티티 그래프를 사용하는 방법은 entityManager.find()와 동일하게 힌트만 추가하면 된다.
List<Order> resultList = entityManager.createQuery("select o from Order o where o.id = :orderId", Order.class)
.setParameter("orderId", orderId)
.setHint("javax.persistence.fetchgraph", entityManager.getEntityGraph("Order.withAll"))
.getResultList();
엔티티 그래프 - 동적 엔티티 그래프
엔티티 그래프를 동적으로 구성하려면 createEntityGraph() 메소드를 사용하면 된다.
EntityGraph<Order> graph = entityManager.createEntityGraph(Order.class);
graph.addAttributeNodes("member");
Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);
Order order = entityManager.find(Order.class, orderId, hints);
subgraph도 동적 엔티티 그래프로 만들 수 있다.
EntityGraph<Order> graph = entityManager.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 = entityManager.find(Order.class, orderId, hints);
'Spring JPA' 카테고리의 다른 글
애플리케이션과 영속성 관리 - 16 (0) | 2021.08.08 |
---|---|
스프링 데이터 JPA - 15 (0) | 2021.07.31 |
객체 지향 쿼리 언어 - 14 (0) | 2021.07.21 |
객체지향 쿼리 언어 - 13 (0) | 2021.07.17 |
값 타입 - 12 (0) | 2021.07.16 |
댓글