JPA는 자바에서 기본으로 제공하는 Collection, List, Set, Map 컬렉션을 지원
사용하는 경우
@OneToMany, @ManyToMany를 사용해서 일대다나 다대다 엔티티 관계를 매핑할 경우
@ElementCollection을 사용해서 값 타입을 하나 이상 보관할 경우
자바 컬렉션 특징
Collection: 자바가 제공하는 최상위 컬렉션, 하이버네이트는 중복을 허용하고 순서를 보장하지 않는다고 가정
Set: 중복을 허용하지 않는 컬렉션, 순서를 보장하지 않음
List: 순서가 있는 컬렉션, 순서를 보장하고 중복을 허용
Map: Key, Value 구조로 되어 있는 특수한 컬렉션
14.1.1 JPA와 컬렉션
하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드로 감싸서 사용
컬렉션을 효율적으로 관리하기 위해 엔티티를 영속 상태로 만들때 원본 컬렉션을 감싸고 있는 내장 컬렉션을 생성해서 내장 컬렉션을 사용하도록 참조를 변경
하이버네이트가 제공하는 내장 컬렉션은 원본 컬렉션을 감싸고 있어 래퍼 컬렉션이라고도 부름
하이버네이트는 이런 특징 떄문에 컬렉션을 사용할때 즉시 초기화를 권장
하이버네이트 내장 컬렉션과 특징
컬렉션 인터페이스 | 내장 컬렉션 | 중복 허용 | 순서
-- | -- | -- | --
Collection, List | PersistentBag | O | X
Set | PersistentSet | X | X
List + @OrderColumn | PersistentList | O | O
14.1.2 Collection, List
Collection, List는 중복 허용. PersistentBag을 래퍼 컬렉션으로 사용
중복을 허용하므로 객체를 추가하는 add() 메소드는 중복된 엔티티가 있는지 비교하지 않고 항상 true 반환, 엔티티를 찾거나 삭제할 때는 equals() 메소드 사용
List<Comment> comments = new ArrayList<Comment>();
...
boolean result = comments.add(data); //단순 추가, 항상 true
comments.contains(comment); //equals 비교
comments.remove(comment); //equals 비교
Collection, List는 엔티티 추가 시에 중복 비교 없이 단순히 저장만 하므로 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않음
14.1.3 Set
Set은 중복을 허용하지 않는 컬렉션, PersistentSet을 컬렉션 래퍼로 사용
HashSet은 중복을 허용하지 않으므로 add() 메소드로 객체를 추가할 때 마다 equals() 메소드로 같은 객체가 있는지 비교
같은 객체가 없으면 객체를 추가하고 true 반환, 이미 있어서 실패하면 false를 반환
해시 알고리즘을 사용하므로 hashcode()도 함께 사용해서 비교
Set<Comment> comments = new HashSet<Comment>();
...
boolean result = comments.add(data); //hashcode + equals 비교
comments.contains(comment); //hashcode + equals 비교
comments.remove(comment); //hashcode + equals 비교
Set은 추가할때 중복된 엔티티가 있는지 비교하므로 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화
14.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>()
...
}
순서가 있는 컬렉션은 데이터베이스에 순서 값도 함께 관리
@OrderColumn의 단점 (→ 실무에 잘 사용X)
@OrderColumn을 Board 엔티티에서 매핑하므로 Comment는 POSITION의 값을 알 수 없으므로 Comment를 INSERT할 때는 POSITION 값이 저장되지 않음. POSITION은 Board.comments의 위치 값이므로, 이 값을 사용해서 POSITION의 값을 UPDATE하는 SQL이 추가로 발생
List를 변경하면 연관된 많은 위치 값을 변경해야함. 2번 인덱스의 row를 삭제하면 3번, 4번 row의 POSITION값을 각각 하나씩 줄이는 UPDATE SQL이 2번 추가로 실행
중간에 POSITION 값이 없으면 조회한 List에는 Null이 보관된다. 2번 인덱스의 row를 강제 삭제하고 다른 row를 수정하지 않으면 List를 조회할 경우 2번 위치에 null 값이 보관되어 컬렉션을 순회할 때 NullPointerException이 발생.
14.1.5 @OrderBy
@OrderBy는 데이터베이스의 ORDER BY절을 사용해서 컬렉션을 정렬
순서용 컬럼을 매핑하지 않아도 가능
@OrderBy는 모든 컬렉션에 사용 가능
@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>();
...
}
14.2 @Converter
컨버터(converter)를 사용하면 엔티티의 데이터를 반환해서 데이터베이스에 저장 가능
// VIP를 Y와N으로 지정하고 싶은 경우
@Entity
public class Member {
@Id
private String id;
private String username;
@Convert(converter=BooleanToYNConverter.class)
private boolean vip;
//Getter, Setter
...
}
@Convert를 적용해서 데이터베이스에 저장되기 직전 BooleanToYNConverter 컨버터가 동작
BooleanToYNConverter.java
@Convert
public class BooleanToYNConverter implements AttributeConverter<Booelan, String> {
@Override
public String convertToDatebaseColumn(Boolean attribute) {
return (attribute != null && attribute) ? "Y" : "N";
}
@Override
public Boolean convertToEntityAttribute(String dbData) {
return "Y".equals(dbData);
}
}
컨버터 클래스는 @Convert 어노테이션을 사용하고 AttributeConvert 인터페이스 구현 필요
AttributeConvert에 제네릭에는 현재 타입과 변환할 타입을 지정 ( <Boolean, String> )
public interface AttributeConverter<X, Y> {
public Y convertToDatabaseColumn (X attribute);
public X convertToEntityAttribute (Y dbData);
}
convertToDatabaseColumn() : 엔티티 데이터를 DB 컬럼에 저장할 데이터로 변환
convertToEntityAttribute() : DB에서 조회한 컬럼 데이터를 엔티티의 데이터로 변환
컨버터는 클래스 레벨에도 설정 가능, 단 이때는 attributeName 속성 사용이 필요 (적용 속성명)
@Entity
@Convert(converter=BooleanToYNConverter.class, attributeName = "vip")
public class Member {
@Id
private String id;
private String username;
private boolean vip;
...
}
14.2.1 글로벌 설정
모든 Boolean 타입에 컨버터를 적용하려면 @Converter(autoApply = true) 옵션을 적용
@Converter(autoApply = true)
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {
...
}
14.3 리스너
JPA 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트 처리가 가능
14.3.1 이벤트 종류
이벤트의 종류와 발생 시점 예시
PostLoad
엔티티가 영속성 컨텍스트에 조회된 직후 또는 refresh를 호출한 후
PrePersist
persist() 메소드를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에 호출
PreUpdate
flush나 commit을 호출해서 엔티티를 DB에 수정하기 직전에 호출
PreRemove
remove() 를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출
삭제 명령어로 영속성 전이가 일어날때도 호출
PostPersist
flush나 commit을 호출해서 엔티티를 DB에 저장한 직후에 호출
식별자가 항상 존재. 식별자 생성 전략이 IDENTITY면 식별자 생성을 위해 persist()를 호출하면서 DB에 엔티티를 저장하므로 이때는 persist()를 호출한 직후에 PostPersist 호출
PostUpdate
flush나 commit을 호출해서 엔티티를 DB에 수정한 직후에 호출
PostRemove
flush나 commit을 호출해서 엔티티를 DB에 삭제한 직후에 호출
14.3.2 이벤트 적용 위치
이벤트는 엔티티에서 직접 받거나 별도의 리스너를 등록해서 받을 수 있음
엔티티에 직접 적용
별도의 리스너 등록
기본 리스너 사용
엔티티에 직접 적용
엔티티에 이벤트가 발생할 때마다 어노테이션으로 지정한 메소드 실행
@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");
}
@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 prePersist(Object obj) {
System.out.println("DuckListener.prePersist obj = [" + obj + "]");
}
@PostPersist
//특정 타입이 확실하면 특정 타입을 받을 수 있다.
private void postPersist(Object obj) {
System.out.println("DuckListener.postPersist obj = [" + obj + "]");
}
}
javax.persistence.ExcludeDefaultListeners : 기본 리스너 무시
javax.persistence.ExcludeSuperclassListeners : 상위 클래스 이벤트 리스너 무시
14.4 엔티티 그래프
엔티티를 조회할 때 연관된 엔티티를 함께 조회하려면 글로벌 fetch 옵션을 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을 중복해서 작성하는 경우가 많음. (JPQL이 데이터 조회뿐아니라 연관 엔티티를 함께 조회하는 기능도 제공하기 때문)
// case 1
select o from Order o
where o.status = ?
// case 2
select o from Order o
join fetch o.member
where o.status = ?
// case 3
select o from Order o
join fetch o.orderItems
where o.status = ?
3가지 JPQL 모두 주문을 조회하는 JPQL이지만 함께 조회할 엔티티에 따라서 다른 JPQL을 사용해야 한다.
엔티티 그래프를 사용하면 연관된 엔티티를 함께 조회하면 되고 JPQL은 데이터를 조회하는 기능만 수행할 수 있다.
14.4.1 Named 엔티티 그래프
// 주문(Order)을 조회할 때 연관된 회원(Member)도 함께 조회하는 엔티티 그래프
**@NamedEntityGraph(name = "Order.withMember", attributeNodes = {
@NamedAttributeNode("member")
})**
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
// 지연로딩 설정이나 엔티티 그래프 설정으로 Order를 조회할 때 연관된 member도 함께 조회 가능
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "MEMBER_ID")
private Member member;
...
}
Named 엔티티 그래프는 @NamedEntityGraph로 정의
name: 엔티티 그래프의 이름을 정의
attributeNodes: 함께 조회할 속성 선택. @NamedAttributeNode를 사용하고 그 값으로 함께 조회할 속성을 선택
둘 이상 정의하려면 NamedEntityGraphs를 사용
14.4.2 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);
Named 엔티티 그래프를 사용하려면 정의한 엔티티 그래프를 em.getEntityGraph("Order.withMember") 를 사용해 찾아오면 됨.
JPA의 힌트 기능을 사용해서 동작. 힌트 키로 javax.persistence.fetchgraph 사용
14.4.3 subgraph
Order → OrderItem → Item 까지 함께 조회하는 경우 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; //주문 상품
...
}
OrderItem → Item은 Order가 관리하는 필드가 아니므로 subgraphs 속성 정의가 필요
@NamedSubgraph를 사용해서 서브 그래프를 정의
14.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();
14.4.5 동적 엔티티 그래프
엔티티 그래프를 동적으로 구성하려면 createEntityGraph() 메소드를 사용
public <T> EntityGraph<T> createEntityGraph(Class<T> rootType);
14.4.6 엔티티 그래프 정리
엔티티 그래프는 항상 조회하는 엔티티의 ROOT에서 시작해야함.
영속성 컨텍스트에 해당 엔티티가 이미 로딩되어 있으면 그 엔티티 그래프 적용되지 않음
fetchgraph, loadgraph의 차이
fetchgraph: 엔티티 그래프에 선택한 속성만 함께 조회
loadgraph: 엔티티 그래프에 선택한 속성뿐만 아니라 글로벌 fetch 모드가 FetchType.EAGER로 설정된 연관관계도 포함해서 함께 조회
14장. 컬렉션과 부가기능
14.1 컬렉션
Collection
: 자바가 제공하는 최상위 컬렉션, 하이버네이트는 중복을 허용하고 순서를 보장하지 않는다고 가정Set
: 중복을 허용하지 않는 컬렉션, 순서를 보장하지 않음List
: 순서가 있는 컬렉션, 순서를 보장하고 중복을 허용Map
: Key, Value 구조로 되어 있는 특수한 컬렉션14.1.1 JPA와 컬렉션
하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드로 감싸서 사용
컬렉션을 효율적으로 관리하기 위해 엔티티를 영속 상태로 만들때 원본 컬렉션을 감싸고 있는 내장 컬렉션을 생성해서 내장 컬렉션을 사용하도록 참조를 변경
하이버네이트가 제공하는 내장 컬렉션은 원본 컬렉션을 감싸고 있어 래퍼 컬렉션이라고도 부름
하이버네이트는 이런 특징 떄문에 컬렉션을 사용할때 즉시 초기화를 권장
하이버네이트 내장 컬렉션과 특징
컬렉션 인터페이스 | 내장 컬렉션 | 중복 허용 | 순서 -- | -- | -- | -- Collection, List | PersistentBag | O | X Set | PersistentSet | X | X List + @OrderColumn | PersistentList | O | O14.1.2 Collection, List
Collection, List는 중복 허용. PersistentBag을 래퍼 컬렉션으로 사용
중복을 허용하므로 객체를 추가하는 add() 메소드는 중복된 엔티티가 있는지 비교하지 않고 항상 true 반환, 엔티티를 찾거나 삭제할 때는 equals() 메소드 사용
Collection, List는 엔티티 추가 시에 중복 비교 없이 단순히 저장만 하므로 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않음
14.1.3 Set
Set은 중복을 허용하지 않는 컬렉션, PersistentSet을 컬렉션 래퍼로 사용
HashSet은 중복을 허용하지 않으므로 add() 메소드로 객체를 추가할 때 마다 equals() 메소드로 같은 객체가 있는지 비교
Set은 추가할때 중복된 엔티티가 있는지 비교하므로 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화
14.1.4 List + @OrderColumn
List 인터페이스에 @OrderColumn을 추가하면 순서가 있는 특수한 컬렉션으로 인식
순서가 있다는 의미는 데이터베이스에 순서 값을 저장해서 조회할 때 사용한다는 의미
하이버네이트 내부 컬렉션인 PersistentList를 사용
순서가 있는 컬렉션은 데이터베이스에 순서 값도 함께 관리
@OrderColumn의 단점 (→ 실무에 잘 사용X)
14.1.5 @OrderBy
14.2 @Converter
@Convert를 적용해서 데이터베이스에 저장되기 직전
BooleanToYNConverter
컨버터가 동작BooleanToYNConverter.java
컨버터 클래스는 @Convert 어노테이션을 사용하고
AttributeConvert
인터페이스 구현 필요AttributeConvert에 제네릭에는 현재 타입과 변환할 타입을 지정 ( <Boolean, String> )
convertToDatabaseColumn()
: 엔티티 데이터를 DB 컬럼에 저장할 데이터로 변환convertToEntityAttribute()
: DB에서 조회한 컬럼 데이터를 엔티티의 데이터로 변환컨버터는 클래스 레벨에도 설정 가능, 단 이때는 attributeName 속성 사용이 필요 (적용 속성명)
14.2.1 글로벌 설정
모든 Boolean 타입에 컨버터를 적용하려면
@Converter(autoApply = true)
옵션을 적용14.3 리스너
14.3.1 이벤트 종류
이벤트의 종류와 발생 시점 예시
PostLoad
refresh
를 호출한 후PrePersist
persist()
메소드를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에 호출PreUpdate
flush
나commit
을 호출해서 엔티티를 DB에 수정하기 직전에 호출PreRemove
remove()
를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출PostPersist
persist()
를 호출하면서 DB에 엔티티를 저장하므로 이때는persist()
를 호출한 직후에PostPersist
호출PostUpdate
PostRemove
14.3.2 이벤트 적용 위치
엔티티에 직접 적용
별도의 리스너 등록
기본 리스너 사용
모든 엔티티의 이벤트를 처리하려면 META-INF/orm.xml에 기본 리스너로 등록
여러 리스너를 등록했을 때 이벤트 호출 순서
더 세밀한 설정
javax.persistence.ExcludeDefaultListeners
: 기본 리스너 무시javax.persistence.ExcludeSuperclassListeners
: 상위 클래스 이벤트 리스너 무시14.4 엔티티 그래프
엔티티를 조회할 때 연관된 엔티티를 함께 조회하려면 글로벌 fetch 옵션을
FetchType.EAGER
로 설정또는 JPQL에서 페치 조인 사용
글로벌 fetch 옵션은 애플리케이션 전체에 영향을 주고 변경할 수 없는 단점.
그래서 일반적으로 글로벌 fetch 옵션은 FetchType.LAZY를 사용하고, 엔티티를 조회할 때 연관된 엔티티를 함께 조회할 필요가 있으면 JPQL의 페치 조인을 사용.
페치 조인을 사용하면 같은 JPQL을 중복해서 작성하는 경우가 많음. (JPQL이 데이터 조회뿐아니라 연관 엔티티를 함께 조회하는 기능도 제공하기 때문)
14.4.1 Named 엔티티 그래프
@NamedEntityGraph
로 정의@NamedAttributeNode
를 사용하고 그 값으로 함께 조회할 속성을 선택NamedEntityGraphs
를 사용14.4.2 em.find()에서 엔티티 그래프 사용
em.getEntityGraph("Order.withMember")
를 사용해 찾아오면 됨.javax.persistence.fetchgraph
사용14.4.3 subgraph
14.4.4 JPQL에서 엔티티 그래프 사용
em.find()와 동일하게 힌트만 추가하면 사용 가능
14.4.5 동적 엔티티 그래프
엔티티 그래프를 동적으로 구성하려면 createEntityGraph() 메소드를 사용
14.4.6 엔티티 그래프 정리
fetchgraph
: 엔티티 그래프에 선택한 속성만 함께 조회loadgraph
: 엔티티 그래프에 선택한 속성뿐만 아니라 글로벌 fetch 모드가 FetchType.EAGER로 설정된 연관관계도 포함해서 함께 조회