1. 컬렉션
- JPA는 자바에서 기본으로 제공하는 Collection, List, Set, Map 컬렉션을 지원하고 다음의 경우에 컬렉션을 사용할 수 있다.
@OneToMany
@ManyToMany
를 사용해서 일대다나 다대다 엔티티 관계를 매핑할 때@ElementCollection
을 사용해서 값 타입을 하나 이상 보관할 때
1.1 JPA와 컬렉션
- 하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이터에서 준비한 컬렉션으로 감싸서 사용한다.
- 하이버네이트는 컬렉션을 효율적으로 관리하기 위해 엔티티를 영속 상태로 만들 때 원본 컬렉션을 감싸고 있는 내장 컬렉션을 생성해서 이 내장 컬렉션을 사용하도록 참조를 변경한다.
1.2 Collection, List
- Collection, List는 중복을 허용하는 컬렉션이고
PersistentBag
을 래퍼 컬렉션으로 사용한다.
@OneToMany
@JoinColumn
private Collection<CollectionChild> collection = new ArrayList<CollectionChild>();
@OneToMany
@JoinColumn
private List<ListChild> list = new ArrayList<ListChild>();
- 객체를 추가하는
add()
메소드는 항상 true 반환하고, 조회나 삭제할 때는equals()
메소드 사용하여 비교한다. - 엔티티를 추가할 때 중복을 비교하지 않고 단순 저장만 하기 때문에, 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않는다.
1.3 Set
- 중복을 허용하지 않는 컬렉션이고
PersistentSet
을 래퍼 컬렉션으로 사용한다. - HashSet은 중복을 허용하지 않으므로
add()
메소드로 객체를 추가할 때 마다equals()
메소드로 같은 객체가 있는지 비교한다. (해시 알고리즘을 사용하므로hashcode()
도 함께 사용해서 비교한다) - 엔티티를 추가할 때 중복 비교를 해야 하므로, 지연 로딩된 컬렉션을 초기화한다.
1.4 List + @OrderColumn
- List 인터페이스에
@OrderColumn
을 추가하면 순서가 있는 특수한 컬렉션으로 인식한다. - 하이버네이트는 내부 컬렉션인
PersistentList
을 사용한다.
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
...
@OneToMany(mappedBy = "board")
@OrderColumn(name = "POSITION")
private List<Comment> comments = new ArrayList<Comment>();
}
//사용
Board board = new Board("title", "content");
em.persist(board);
Comment comment1 = new Comment("comment1");
comment1.setBoard(board); // POSITION 0
em.persist(comment1);
Comment comment2 = new Comment("comment1");
comment2.setBoard(board); // POSITION 1
em.persist(comment2);
- 데이터베이스에서 순서 값도 함께 관리한다. List의 순서 값을 POSITION이라는 컬럼에 저장하게 되는 것이고, 이는 일대다 관계의 특성에 따라 다쪽에 저장하게 된다.
- 실무에서 사용하게에는 단점이 많기 때문에 POSITION값을 직접 관리하거나
@OrderBy
사용을 권장
단점
@OrderColumn
을 Board 엔티티에서 매핑하므로 Comment는 POSITION의 값을 알 수 없다. 그래서 Comment를 INSERT 할 때는 POSITION 값이 저장되지 않는다. POSITION은 Board.comments의 위치 값이므로, 이 값을 사용해서 POSITION의 값을 UPDATE 하는 SQL이 추가로 발생한다.- 요소가 하나만 변경돼도 모든 위치값이 변경된다. 예를 들어, 첫 번째 댓글을 삭제하면 그 뒤의 댓글 수만큼 POSTION을 변경하는 쿼리가 실행된다.
- 중간에 POSITION 값이 없으면 null이 저장된다. 예를 들어, 강제로 0,1,2의 POSITION 값을 0,2,3으로 변경하면 1번 위치에 null이 보관된 컬렉션이 반환된다. 이러면 NullPointerException이 발생한다.
1.5 @OrderBy
@OrderBy
는 데이터베이스의 ORDER BY절을 사용해서 컬렉션을 정렬한다. 따라서 순서용 컬럼을 매핑하지 않아도 된다.@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>();
...
}
2. @Converter
- 컨버터를 사용하면 엔티티의 데이터를 변환해서 DB에 저장할 수 있다.
- 예를 들어, 회원의 VIP 여부를 자바의 boolean타입을 사용하며, DB에는 문자 Y/N으로 저장하고 싶을 때 컨버터를 사용한다.
@Entity
class Member{
@Id @GeneratedValue
private Integer id;
@Convert(converter=BooleanToYNConverter.class)
private boolean vip;
}
// 컨버터
@Converter
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 인터페이스의 다음 2개의 메소드를 구현해야 한다.
convertToDatabaseColumn()
: 엔티티의 데이터를 데이터베이스 컬럼에 저장할 데이터로 변환convertToEntityAttribute()
: 데이터베이스에서 조회한 컬럼 데이터를 엔티티의 데이터로 변환
- 컨버트는 클래스 레벨에도 설정할 수 있다. 이때는 attributeName 속성으로 어떤 필드에 컨버터를 적용할지 명시해야 한다.
@Entity
@Convert(converter=BooleanToYNConverter.class, attributeName = "vip")
public class Member {
@Id
private String id;
private boolean vip;
}
2.1 글로벌 설정
- 모든 Boolean 타입에 컨버터를 적용하려면
@Converter(autoApply = true)
옵션을 적용하면 된다. - 엔티티에서
@Converter
어노테이션을 지정하지 않아도 모든 Boolean 타입에 대해 자동으로 컨버터가 적용된다
@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);
}
}
3. 리스너
- JPA 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트를 처리할 수 있다.
3.1 이벤트 종류
- PostLoad : 엔티티가 영속성 컨텍스트에 조회된 직후 또는 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을 호출해서 엔티티를 데이터베이스에 삭제한 직후에 호출된다.
3.2 이벤트 적용 위치
- 이벤트는 엔티티에서 직접 받거나 별도의 리스너를 등록해서 받을 수 있다.
엔티티에 직접 적용
- 이벤트가 발생할 때마다 어노테이션으로 지정한 메소드가 실행
@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");
}
}
별도의 리스너 등록
- 리스너는 대상 엔티티를 파라미터로 받을 수 있다. 반환 타입은 void로 설정해야 한다.
@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 + "]");
}
}
4. 엔티티 그래프
- 엔티티를 조회할 때 연관된 엔티티들을 함께 조회하려면 글로벌 fetch 옵션은 FetchType.LAZY를 사용 + JPQL 페치 조인 사용
- 이 경우 같은 JPQL을 중복해서 작성하는 경우가 많다.
- 엔티티 그래프 기능은 엔티티 조회시점에 연관된 엔티티를 함께 조회하는 기능이다.
4.1 Named 엔티티 그래프
@NamedEntityGraph(name = "Order.withMember", attributeNodes = {
@NamedAttributeNode("member")
})
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchTYpe.LAZY, optional = false)
@JoinCloumn(name = "MEMBER_ID")
private Member member;
...
}
- Named 엔티티 그래프는
@NamedEntityGraph
로 정의- name : dpsxlxl rmfovmdml dlfmadmf wjddml
- attributeNodes ; 함께 조회할 속성을 선택
@NamedAtrributeNode
를 사용
- Order.member가 지연 로딩으로 설정되어 있지만 엔티티 그래프에서 함께 조회할 속성으로 member를 선택했으므로 Order를 조회할 때 member도 함께 조회할 수 있다.
4.2 em.find()
에서 엔티티 그래프 사용
- 엔티티 그래프는 JPA의 힌트 기능을 사용해서 동작한다.
- 힌트의 키로는
javax.persistence.fetchgraph
를 사용하고 값으로는 찾아온 엔티티 그래프를 사용하면 된다.
EntityGraph graph = em.getEntityGraph("Order.withMember");
Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);
Order order = em.find(Order.class, orderId, hints);
4.3 subgraph
- Order → OrderItem → Item까지 함께 조회 가능
- OrderItem → Item은 Order가 관리하는 필드가 아니기 때문에
@NamedSubgraph
로 서브 그래프를 정의
@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
private Long id;
@ManyToOne(fetch = FetchTYpe.LAZY, optional = false)
@JoinCloumn(name = "MEMBER_ID")
private Member member;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<OrderItem>();
...
}
@Entity
public class OrderItem {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ITEM_ID")
private Item item;
...
}
4.4 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();
4.5 동적 엔티티 그래프
- 엔티티 그래프를 동적으로 구성하려면
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);
4.6 엔티티 그래프 정리
이미 로딩된 엔티티
- 영속성 컨텍스트에 해당 엔티티가 이미 로딩되어 있으면 엔티티 그래프가 적용되지 않는다.
- 아직 초기화되지 않은 프록시에는 엔티티 그래프가 적용된다.
Order order = em.find(Order.class, orderId); // 이미 조회
hints.put("javax.persistence.fetchgraph", em.getEntityGraph("Order.withMember"));
// 처음 조회한 order1과 같은 인스턴스 반환
Order order2 = em.find(Order.class, orderId, hints);
fetchgraph, loadgraph의 차이
- fetchgraph는 엔티티 그래프에 선택한 속성만 함께 조회한다.
- loadgraph 속성은 엔티티 그래프에 선택한 속성뿐만 아니라 글로벌 fetch 모드가
FetchType.EAGER
로 설정된 연관관계도 포함해서 함께 조회한다.
'자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 13장 - 웹 애플리케이션과 영속성 관리 (0) | 2023.07.26 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 12장 - 스프링 데이터 JPA (0) | 2023.07.26 |
[자바 ORM 표준 JPA 프로그래밍] 10.4 - QueryDSL (0) | 2023.07.22 |
[자바 ORM 표준 JPA 프로그래밍] 10.2 - JPQL (0) | 2023.07.22 |
[자바 ORM 표준 JPA 프로그래밍] 9장 - 값 타입 (0) | 2023.07.22 |