JPA는 자바에서 기본으로 제공하는 Collection, List, Set, Map 컬렉션을 지원하고 아래의 경우에 사용 가능함.
@OneToMany, @ManyToMany를 사용해 일대다 또는 다대다 엔티티 관계를 매핑할 때@ElementCollection을 사용해 값 타입을 하나 이상 보관할 때예시:
@Entity
public class Team {
@Id
private String id;
@OneToMany
@JoinColumn
private Collection<Member> members = new ArrayList<Member>();
}
아래의 코드를 통해 Team을 영속 상태로 만들면
Team team = new Team();
System.out.println("before persist = " + team.getMembers().getClass());
System.out.println("after persist = " + team.getMembers().getClass());
출력 결과가 아래와 같다.
before persist = class.java.util.**ArrayList**
after persist = class.org.hibernate.collection.internal.**PersistentBag**
결과를 보면 원래 ArrayList 타입이었던 컬렉션이 엔티티를 영속상태로 만든 직후 하이버네이트가 제공하는 PersistentBag 타입으로 변경됨.
| 컬렉션 인터페이스 | 내장 컬렉션 | 중복 허용 | 순서 보관 |
|---|---|---|---|
| Collection.List | PersistenceBag | O | X |
| Set | PersistenceSet | X | X |
| List + @OrderColumn | PersistentList | O | O |
//PersistenceBag
@OneToMany
Collection<Member> collection = new ArrayList<Member>();
//PersistenceBag
@OneToMany
List<Member> list = new ArrayList<Member>();
//PersistenceSet
@OneToMany
Set<Member> set = new HashSet<Member>();
//PersistentList
@OneToMany @OrderColumn
List<Member> orderColumnList = new ArrayList<Member>();
ArrayList로 초기화add() 메소드는 내부에서 어떤 비교도 하지 않고 항상 true를 반환.equals() 메소드를 사용.@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
@OneToMany
@JoinColumn
private Collection<CollectionChild> collection =
new ArrayList<CollectionChild> ();
@OneToMany
@JoinColumn
private List<ListChild> list = new ArrayList<ListChild>();
...
}
Collection, List는 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고 단순히 저장만 하면 됨. 따라서 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않음.
HashSet으로 초기화equals() 메소드로 같은 객체가 있는지의 여부를 비교. (HashSet의 경우, 해시 알고리즘을 사용하기에 hashcode()도 함께 사용해 비교)@Entity
public class Parent {
@OneToMany
@JoinColumn
private Set<SetChild> set = new HashSet<SetChild>();
...
}
Set은 엔티티를 추가할 때 중복된 엔티티가 있는지 비교해야하기에 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화함.
@OrderColumn을 추가하면 순서가 있는 특수한 컬렉션으로 인식함@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
private String content;
@OneToMany(mappedBy = "board")
@OrderColumn(name = "POSITION")
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 Baord board;
...
}
@OrderColumn을 추가. 따라서 Board.comments는 순서가 있는 컬렉션으로 인식됨list.add(1, data1); → 1번 위치에 data1 저장list.get(10); → 10번 위치에 있는 값을 조회@OrderColumn의 name 속성에 POSITION이라는 값을 줌.실무에서 잘 사용하지 않는 이유의 단점들이 존재
@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;
...
}
사용 시 엔티티의 데이터를 변환해 데이터베이스에 저장
예) 회원의 VIP 여부를 boolean 타입을 사용할 때. JPA를 사용하면 0 또는 1인 숫자로 저장됨.
→ 이때 데이터베이스에 숫자 대신 Y/N으로 저장하려면 컨버터를 사용하면 됨.
@Entity
public class Member {
@Id
private String id;
private String username;
@Convert(converter = BooleanToYNConverter.class)
private boolean vip;
...
}
@Convert
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 인터페이스를 구현해야함. 또 제네릭에 현재 타입과 반환할 타입을 지정해야함.아래 두 가지 메소드를 구현해야함.
convertToDatabaseColumn(): 엔티티의 데이터를 데이터베이스 컬럼에 저장할 데이터로 변환.convertToEntityAttribute(): 데이터베이스에서 조회한 컬럼 데이터를 엔티티 데이터로 변환.모든 Boolean 타입에 컨버터를 적용하기 위해서는 @Converter(autoApply = true) 옵션을 적용하면 됨.
**@Convert(autoApply = true)**
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 | 사용할 컨버터를 지정 | |
| attributeName | 컨버터를 적용할 필드를 지정 | |
| disableConversion | 글로벌 컨버터나 상속 받은 컨버터를 사용하지 않음 | false |
JPA 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트를 처리할 수 있음.
예) 모든 엔티티를 대상으로 언제, 어떤 사용자가 삭제를 요청했는지 모두 로그로 남겨야하는 요구사항
→ 모든 애플리케이션의 삭제 로직마다 로그를 남기는 것은 효율적.

persist() 메소드를 호출해 엔티티를 영속성 컨텍스트에 관리하기 직전 호출. (식별자 생성 전략 사용 시 식별자는 아직 존재X). 새로운 인스턴스 merge 시에도 수행flush 또는 commit을 호출해 엔티티를 데이터베이스에 수정하기 직전 호출remove() 메소드를 호출해 엔티티를 영속성 컨텍스트에서 삭제하기 직전 호출. 삭제 명령어로 영속성 전이가 일어날 때도 호출. (orphanRemoval에 대해서는 flush/commit할 때에 호출됨)이벤트는 엔티티에서 직접 받거나 별도의 리스너를 등록할 수 있음.
엔티티에 이벤트가 발생할 때마다 어노테이션으로 지정한 메소드가 실행됨.
@Entity
public class Duck {
@Id @GeneratedValue
public 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);
}
}
리스너의 경우 대상 엔티티를 파라미터로 받을 수 있다. 이때 반환 타입은 void로 설정해야함.
@Entity
**@EntityListeners(DuckListener.class)**
public class Duck {...}
public class DuckListener {
@PrePersist
//특정 타입인 것이 확실하면 특정 타입을 받을 수 있음.
public void prePersist(Object obj) {
System.out.println("Duck.prePersist obj" + obj);
}
@PostPersist
//특정 타입인 것이 확실하면 특정 타입을 받을 수 있음.
public void postPersist(Object obj) {
System.out.println("Duck.postPersist obj=" + obj);
}
}
모든 엔티티의 이벤트를 처리하기 위해서는 META-INF/orm.xml에 기본 리스너로 등록하면 됨.
여러 리스너를 등록하면 아래의 순서대로 이벤트가 호출됨.
기본 리스너 → 부모 클래스 리스너 → 리스너 → 엔티티
+) 더 세밀한 설정
엔티티 조회 시 연관 엔티티를 함께 조회하기 위해서는 글로벌 옵션을 FetchType.EAGER로 설정
@Entity
class Order {
@ManyToOne(fetch = FetchType.EAGER)
Member member;
...
}
또는 JPQL에서 페치 조인을 사용하면 됨.
select o from Order o join fetch o.member
⇒ 글로벌 fetch 옵션의 경우 애플리케이션 전체에 영향을 주고 변경할 수 없는 단점이 존재.
→ 따라서 글로벌 fetch 옵션은 FetchType.LAZY를 사용하고 엔티티를 조회할 때 연관 엔티티를 함께 조회할 필요가 있으면 JPQL의 페치조인을 사용함
예) 주문 상태를 검색조건으로 주문 엔티티를 조회하는 JPQL 작성
⇒ 3가지 JPQL 모두 주문을 조회하는 JPQL이지만, 함께 조회할 엔티티에 따라 다른 JPQL을 사용해야함.
**@NamedEntityGraph(name = "Order.withMember", attributeNodes = {
@NamedAttributeNode("member")
})**
@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;
...
}
@NamedEntityGraph로 정의.@NamedAttributeNode를 사용하고 그 값으로 함께 조회할 속성을 선택.@NamedEntityGraphs 이용EntityGraph graph = em.getEntity("Order.withMember");
Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);
Order order = em.find(Order.class, orderId, hints);
em.getEntityGraph("Order.withMember")를 통해 찾아오면 됨em.find()를 통해 엔티티를 조회할 때 힌트 정보 역시 포함함.목표: Order → OrderItem → Item 조회.
Order → OrderItem의 경우에 Order가 관리하는 필드지만, OrderItem → Item은 Order가 관리하는 필드가 아님 ⇒ 이런 경우에 subgraph를 사용
@NameEntityGraph(name = "Order.withAll", attributeNodes = {
@NameAttributeNode("member"),
@NameAttributeNode(value = "orderItems", subgraph = "orderItems")
},
**subgraphs = @NamedSubgraph(name = "orderItems", attributeNodes = {
@NameAttributeNode("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", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<OrderItem>();
...
}
@Entity
@Table(name = "ORDER_ITEM")
public class OrderItem {
@Id @GeneratedValue
@Column(name = "ORDER_ITEM_ID")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ITEM_ID")
private Item item; //주문 상품
...
}
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();
엔티티 그래프를 동적으로 구성하기 위해서는 createEntityGraph() 메소드를 사용
public <T> EntityGraph<T> createEntityGraph(Class<T> rootType);
예) 앞의 Named 엔티티 그래프의 동적 구성
**EntityGraph<Order> graph = em.createEntityGraph(Order.class);**
graph.addAttributeNodes("member");
Map hints = new HashMap()
hints.get("javax.persistence.fetchgraph", graph);
Order order = em.find(Order.class, orderId, hints);
createEntityGraph() 메소드를 사용해 동적으로 엔티티 그래프를 만든 후 addAttributeNodes()를 통해 Order.member 속성을 엔티티 그래프에 포함예) 동적 엔티티 그래프 구성 + subgraph 추가
EntityGraph<Order> graph = em.createEntityGraph(Order.class);
graph.addAttributeNodes("member");
**Subgraph<OrderItem> orderItems = graph.addSubgraph("orderItems");**
**orderItems.addAttributeNodes("item");**
Map hints = new HashMap()
hints.get("javax.persistence.fetchgraph", graph);
Order order = em.find(Order.class, orderId, hints);
Order order1 = em.find(Order.class, orderId); //이미 조회
hints.put("javax.persistence.fetchgraph", em.getEntityGraph("Order.withMember"));
Order order2 = em.find(Order.class, orderId, hints); ⇒ 위의 경우 조회된 order2에는 엔티티 그래프가 적용되지 않고 처음 조회한 order1과 같은 인스턴스가 반환됨