컬렉션과 부가기능

원종서·2022년 2월 10일
1

JPA

목록 보기
1/13

컬렉션

@OneToMany , @ManyToMany 를 사용해 엔티티관계를 매핑할때
@ElementCollection 을 사용해 값 타입을 하나 이상 보관할때

  • Jpa 명세에는 자바 컬렉션 인터페이스에 대한 특별한 언급이 없다. 밑의 예시들은 하이버네이트 구현체 기준이다.

JPA 와 컬렉션

하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 래핑한다.
이유는 컬렉션을 효율적으로 관리하기 위해 엔티티를 영속상태로 만들때 원본 컬렉션을 래핑하고 있는 내장 컬렉션을 생성하고 이 내장 컬렉션을 사용하도록 참조를 변경함. (래퍼 컬렉션이라고 부른다)

Collecion, List 인터페이스

순서를 보장하지 않고 중복을 허용하는 컬렉션이며, PersistentBag 을 래퍼 컬렉션으로 사용함.

@OneToMany
@JoinCollection
private Collection<Parent> collection = new ArrayList<>();

@OneToMany
@JoinCollection
private List<Parent> collection = new ArrayList<>();

컬렉션의 엔티티 중복을 허용하기 때문에, 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않는다.
(중복을 허용하기에 컬렉션 내 엔티티를 조사할 필요가 없음)

Set

중복을 허용하지 않고, PersistentSet 을 컬렉션 래퍼로 사용

@OneToMany
@JoinCollection
private Set<SetChild> set= new HashSet<>();

중복을 허용하지 않기에 add()로 추가할 때마다 eqauls() 메서드로 세트안 객체가 있는지 비교한다.
따라서 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화 한다.

List + @OrderColumn

@OrderColumn는 순서가 있는 특수한 컬렉션으로 인식.
"순서가 있다" 는 데베에 순서 값을 저장해서 조회할 때 사용한다는 의미

@Entity
public class Board {
	@Id @GeneratedValue
    private Long id;
    
    ...
    
    @OneToMany(mapped ="board")
    @OrderColumn(name="POSITION)
    private List<Comment> comments = new ArrayList<>(); // comments 는 순서가 있는 컬렉션
    ...
    
@Entity
public class Comment {
	@Id @GeneratedValue
    private Long id;
    
    @ManyToOne
    @JoinColumn(name="BOARD_ID")
    private Board board;
    ...

@OrderColumn 을 사용한 엔티티는 일대다 관계에서 '일' 에 해당하는 엔티티에 있다.
테이블의 일대다 관계의 특성한 위치 값은 '다' 쪽에 저장해야한다.
따라서 위의 예시에서 위치 정보는 '다' 쪽인 Comment 테이블에 저장된다.

Comment 테이블의 컬럼을 보면
position 컬럼이 존재한다. 만일 position 컬럼의 0번째 행의 값은 comments.get(0) 해서 나오는 값이다.

Board board = new Board("title1", "content1");
em.persist(board);

Comment comment1 = new Comment("comment1");
comment1.setBoard(board);
board.getComments().add(comment1) ; //POSTIOIN 0
em.persist(comment1);

Comment comment2 = new Comment("comment2");
comment2.setBoard(board);
board.getComments().add(comment2) ; //POSTIOIN 2
em.persist(comment2);

단 @OrderColumn 은 단점이 많다.

@OrderBy

@OrderBy는 데베의 ORDER BY절을 사용해서 컬렉션을 정렬한다 따라서 순서용 컬렉션을 매핑하지 않아도 된다.

@Entity
public class Team{
	@Id
    Long id;
    
    @OneToMany(mapped ="team") 
    @OrderBy("username desc, id asc")  // <--
    private Set<Member> members = new HashSet<>();
    
    ...
    
@Entity
public class Member{
	@Id
    Long id;
    
    @Column(name ="MEMBER_NAME")
    String username;
    
    @ManyToOne
   	Team team;
    

@OrderBy 의 값은 JPQL 의 order by 절처럼 엔티티의 필드를 대상으로 한다.

Team team = em.find(Team.class, team.getId());
team.getMembers().size(); // - 초기화

SELECt M.* FROM MEMBER M WHERE M.TEAM_ID = ? 
ORDER BY M.MEMBER_NAME = ? , M.ID ASC

@Converter

컨버터를 사용하면 엔티티의 데이터의 변환해서 데베에 저장할 수 있다.

예를 들어 VIP 여부를 boolean 타입을 사용하고 싶다고 하면, JPA를 사용하면 boolean 타입은 방언에 따라 다르지만, 데베에 저장될 때 0 , 1 인 숫자로 로 저장된다.

이 떄 'Y', 'N' 로 저장하고 싶으면 컨버터를 사용한다.

@Entity
public class Member{
	@Id
    Long id;
    String username;
    
	@Convert(coverter=BooleanToYNConverter.class)
    private boolean vip;
    ...
    

@Converter 
// @Converter(autoApply = true) 모든 Boolean 타입에 컨버터를 적용하려면 글로벌 설정..
// 글로벌 설정을 하면 위와 같이 클래스 내 불린타입에 @Coverter을 따로 명시하지 않아도 된다.
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);
    }
}

클레스 타입에도 적용할 수 있다.

@Entity
@Converter(converter=BooleanToYNConverter.class, attributeName = "vip")
public class Member{
	@Id
    Long id;
    String username;
    
    private boolean vip;
    ...
    

리스너

리스너의 종류

  1. PostLoad : 엔티티가 영속성 컨텍스트에 조회된 직 후 또는 reflash를 호출 한 후
  2. PrePersist : persist 메소드 호출해서 엔티티를 영속성 컨텍시트에 관리하기 직전, 식별자 생성 전략 사용 시 식별자 존재하지 않음. 새로운 엔티티 merge 시
  3. PreUpload : flash, commit 등 엔티티를 데베에 수정 직전 호출
  4. PreRemove : remove 메서드 호출 해서 엔티티를 영속성 컨텍스트에서 삭제하기 전, 영속성 전이가 일어날 때도
  5. PostPersist : flush, commit 을 호출 해서 데베에 저장된 지후, 식별자 존재.
  6. PostUpload
  7. PostRemove

리스너 위치

  1. 엔티티 직접 적용
  2. 별도의 리스너 등록
  3. 기본 리스너 사용

엔티티 직접 적용

@Entity
public class Duck {
	@Id
    Long id;
    
    @PrePersist
    void prePersist(){
    	sout(id); // null
    }
 
 	@PostPersist
    void postPersist(){
    	sout(id); // id =1
    }
    
    
    

별도의 리스너 등록

@Entity
@EntityListeners(DuckListener.class)
public class Duck {}

public class DuckListener {
	@PrePersist
    // 특정 타입 받을 수 있다.
    void prePersist(Object obj){
    	sout(obj.id); 
    }
 
 	@PostPersist
    void postPersist(Object obj){
    	sout(obj.id); 
    }    

여러 리스너를 등록 했을 시 호출 순서

  • 기본 리스너 -> 부모 클래스 리스너 -> 리스너 -> 엔티티

더 세밀한 설정

@ExcludeDefaultListeners 기본 리스너 무시
@ExcludeSuperclassListeners 상위 클래스 리스너 무시

엔티티 그래프

엔티티를 조회할 때 연관된 엔티티들을 함께 조회하려면 글로벌 fetch옵션을 FetchType.EAGER 로 설정하거나

페치조인을 사용한다. (select o from Order o join fetch o.member )

  • 글로벌 페치 옵션은 앱 전체에 영향을 주고 변경할 수 없는 단점이 있다. 그래서 글로벌 페치 옵션은 LAZY를 사용하고,
    엔티티를 조회할 때 연관된 엔티티를 함께 조회할 필요가 있다면 페치 조인을 사용한다.

JPQL 이 데이터를 조회하는 기능뿐아니라, 연관된 엔티티도 함께 조회 함으로 비슷한 JPQL 문이 여러개 작성해야하는 불편함이 생긴다.

이에 대한 해결책으로 JPA 2.1에 추가된 엔티티 그래프 기능을 사용하면 엔티티를 조회하는 시점에 함께 조회할 연관된 엔티티를 선택할 수 있다.

SELECT o FROM Order o where o.status = ?
하나의 쿼리문으로 연관된 엔티티를 선택적으로 사용할 수 있다.

엔티티 그래프 기능은 엔티티 조회 시점에 연관된 엔티티들을 함께 조회하는 기능이다
1. 정적으로 정의하는 Named 엔티티 그래프
2. 동적으로 정의하는 엔티티 그래프

Named 엔티티 그래프

  • Orders을 조회할 떄 연관된 Member도 함께 조회하는 엔티티 그래프 사용 예시.
@NamedEntityGraph(name ="Order.withMember", attributeNodes = {
	@NamedAttributeNode("member")
})
@Entity
public class Orders{
	@Id @GeneratedValue
    private Long orderId;
    
    @ManyToOne(fetch = FetchType.LAZY , optional = false)
    @JoinColumn(name ="MEMBER_ID")
    Member member;
}

@NamedEntityGraph 의
name : 엔티티 그래프의 이름 정의
attributeNodes : 함께 조회할 속성 선택

em.find()에서 엔티티 그래프 사용

EntityGraph graph = em.getEntityGraph("Order.withMember");

Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);

Orders order = em.find(Orders.class, ordersId hints);

Named 엔티티 그래프를 사용하려면 힌트 기능을 사용해서 동작한다.

select o.*, m.*
from 
	Orders o
inner join
	Member m
    	on o.member_id = m.member_id
where
	o.Order_id = ?

subgraph

orders -> orderItem -> item 조회.
이처럼 부모가 자식의 자식까지 함께 조회하는 방법이다. (부모와 자식의 자식과의 관계는 없다.)

@NameEntityGraph(name= "Order.withAll", attributeNodes ={
	@NameAttributeNode("member"),
    @NameAttributeNode(value = "orderItems", subgraph="orderItems")
    },
    subgraphs = @NamedSubgraph(name ="orderItems" , attributeNodes ={
    	@NamedAttributeNode("item")
   })
)
@Entity
@Table(name ="ORDERS")
public class Order {
	@Id
    Long OrderId;
    
    @ManyToOne(fetch = FatchType.LAZY optional =true)
    @JoinColumn(name ="MEMBER_ID")
    Member member;
    
    @OneToMany(mapped ="order", cascade = CascadeType.ALL)
    List<OrderItem> orderItems = new ArrayList<>();
    ...
    
@Entity
@Table(name="ORDER_ITEM")
public class OrderItem {
	@Id
    @Column(name ="ORDER_ITEM_ID")
    Long id
    
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name ="Item_ID")
    Item item;
    
    ..,

Order.withAll 이라는 네임드 엔티티 그래프는

Order -> Member ,Order -> OrderItem, OrderItem -> Item 그래프를 함께 조회한다.

Map hint = new HashMap();
hints.put("javax.persistence.fetchgraph", em.getEntityGraph("Order.withAll"));

Order order = em.find(Order.class, orderId, hints);

select o.*, m.*, oi.*, i.*
from Order o 
inner join 
	Member m
    	On o.Member_id = m.member_id
left outer join
	Order_Item oi
    	ON  o.order_id = oi.order_id 
left outer join
	Item i
    	on oi.item_id = i.item_id
where
	o.order_id = ?

JPQL에서 엔티티 그래프 사용

List<Order> resultList = 
	em.createQuery("select o from Order o where o.id = :orderId , Order.class)
      .setParameter("orderId",orderId)
      .setHint("javax.persistence.fetchgrpah, em.getEntityGraph("Order.withAll")
      .getResultList();
    	

JPQL에서는 엔티티 그래프를 사용할 때 항상 외부조인을 사용한다. 만약 내부 조인을 사용하려면
select o from Order o join fetch o.member where o.id = :orderId
를 사용한다.

동적 엔티티 그래프

createEntityGraph() 를 사용하면 된다.

EntityGraph <Order> graph = em.createEntityGraph(Order.class);
graph.addAttributeNode("member');

Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);

Order order =em.find(Order.class, orderId, hints);

subgraph 기능까지 동적 엔티티 그래프 구현

EntityGraph<Order> graph = em.createEntityGraph(Order.class);
graph.addAttributeNode("member");

Subgraph<OrderItem> orderItems = graph.addSubgraph("orderItems");
orderItems.addAttributeNodes("item");

Map hint = new HashMap();
hint.put("javax.persistence.fetchgraph", graph);

Order order = em.find(Order.class, orderId, hint);

정리

  1. ROOT 에서 시작.
    엔티티 그래프는 항상 조회하는 엔티티의 ROOT 에서 시작해야한다.

  2. 이미 로딩된 엔티티
    영속성 컨텍스트에 해당 엔티티가 이미 로딩되어 있으면 엔티티 그래프 적용되지 앟는다. ( 초기화 되지 않은 프록시는 적용된다.)

Order order1= em.find(Order.class, orderId);
hint.put("java.persistence.fetchgraph", em.getEntityGraph("Order.withMember"));

Order order2 =em.find(Order.class , orderId, hints);

이 경우 order2는 엔티티 그래프가 적용되지 않고, order1 과 같은 인스턴스 반환된다.

3.fetchgraph vs loadgraph
fetchgraph 는 엔티티 그래프에 선택한 속성만 함께 조회함.
반면에 loadgraph 는 글로벌 페치가 EAGER 로 설정된 연관관계도 포함해서 함께 조회한다.

0개의 댓글