본문 바로가기
Spring JPA

컬렉션과 부가 기능 - 17

by 홍굴이 2021. 8. 10.

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 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트를 처리할 수 있다.

  1. PostLoad : 엔티티가 영속성 컨텍스트에 조회된 직후 또는 refresh를 호출한 후(2차 캐시에 저장되어있어도 호출)
  2. PrePersist : persist() 메소드를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에 호출 / 새로운 인스턴스를 merge할 때도 수행
  3. PreUpdate : flush나 commit을 호출해서 엔티티를 데이터베이스에 수정하기 직전에 호출
  4. PreRemove: remove() 메소드를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출 / 삭제 명령어로 영속성 전이가 일어날 때도 호출
  5. PostPersist : flush나 commit츨 호출해서 엔티티를 데이터베이스에 저장한 직후에 호출
  6. PostUpdate : flush나 commit을 호출해서 엔티티를 데이터베이스에 수정한 직후에 호출
  7. 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

댓글