하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 감싸서 사용합니다.
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
@OneToMany
@JoinColumn
private Collection<Member> members = new ArrayList<>();
// getter, setter
}
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Team team = new Team();
System.out.println("Before Persist = " + team.getMembers().getClass());
em.persist(team);
System.out.println("================");
System.out.println("After Persist = " + team.getMembers().getClass());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
그리고 위의 코드처럼 Team
엔티티를 Persist
하기전에도 출력해보고 Persist
를 한 후에도 출력해보겠습니다.
그러면 위와 같이 영속 상태 전에는 ArrayList
였는데, 영속 상태로 만든 직후에 하이버네이트가 제공하는 PersistentBag
타입으로 변경된 것을 볼 수 있습니다. 하이버네이트는 컬렉션을 효율적으로 관리하기 위해 엔티티를 영속 상태로 만들 때 원본 컬렉션을 감싸고 있는 내장 컬렉션을 생성해서 이 내장 컬렉션을 사용하도록 참조를 변경합니다.
하이버네이트는 이러한 특징 때문에 컬렉션을 사용할 때 즉시 초기화해서 사용하는 것을 권장합니다.
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
@OneToMany
@JoinColumn
private Collection<Member> members = new ArrayList<>();
@OneToMany
@JoinColumn
private List<Member> members = new ArrayList<>();
}
Collection, List
인터페이스는 중복을 허용하는 컬렉션입니다. 즉, add()
메소드는 내부에서 어떤 비교도 하지 않고 항상 true를 반환합니다. 같은 엔티티가 있는지 찾거나 삭제할 때는 equals()
메소드를 사용합니다. Collection, List는 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고 단순히 저장만 하면 됩니다. 따라서 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않습니다.
Set은 중복을 허용하지 않는 컬렉션입니다.
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
@OneToMany
@JoinColumn
private Set<Member> members = new HashSet<>();
}
HashSet
은 중복을 허용하지 않으므로 add()
메소드로 객체를 추가할 때마다 equals()
메소드로 같은 객체가 있는지 비교합니다. Set은 엔티티를 추가할 때 중복된 엔티티가 있는지 비교해야 하기 때문에 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화 합니다.
컨버터(converter)를 사용하면 엔티티의 데이터를 변환해서 데이터베이스에 저장할 수 있습니다. 예를들어 회원의 VIP 여부를 자바의 boolean 타입을 사용하고 싶을 때, 어떤 DB는 0, 1로 저장하고 어떤 DB는 true, false로 저장할 것인데요. 이 때 만약에 Y/N 으로 저장하고 싶을 때 JPA에서 사용할 수 있는 것이 @Converter
입니다.
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int age;
private String username;
@Convert(converter = BooleanToYNConverter.class)
private boolean vip;
}
위와 같이 @Convert
를 사용하면 vip
필드가 데이터베이스가 저장되기 직전에 BooleanToYNConverter
가 동작합니다.
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);
}
}
컨버터는 AttributeConverter
인터페이스를 구현해야 합니다. 인터페이스에서 구현해야 하는 메소드는 2가지인데요.
- convertToDatabaseColumn() : 엔티티의 데이터를 데이터베이스 컬럼에 저장할 데이터로 반환합니다. true면 Y를 false면 N을 반환하도록 했다.
- convertToEntityAttribute(): 데이터베이스에서 조회한 컬럼 데이터를 엔티티의 데이터로 변환했습니다. 문자 Y면 true를 아니면 false를 반환합니다.
모든 엔티티를 대상으로 언제 어떤 사용자가 삭제를 요청했는지 모두 로그로 남겨야 하는 요구사항이 있다면 JPA 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트를 처리할 수 있습니다.
PostLoad
: 엔티티가 영속성 컨텍스트에 조회된 직후 또는 refresh를 호출한 후PrePersist
: persist() 메소드를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에 호출됩니다. 식별자 생성 전략을 사용한 경우 엔티티에 식별자는 아직 존재하지 않습니다.PreUpdate
: flush나 commit을 호출해서 엔티티를 데이터베이스에 수정하기 직전에 호출합니다.PreRemove
: remove() 메소드를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출됩니다.PostPersist
: flush나 commit을 호출해서 엔티티를 데이터베이스에 저장한 직후에 호출됩니다. 식별자가 항상 존재합니다.PostUpdate
: flush나 commit을 호출해서 엔티티를 데이터베이스에 수정한 직후에 호출됩니다.PostRemove
: flush나 commit을 호출해서 엔티티를 데이터베이스에 삭제한 직후에 호출됩니다.
엔티티를 조회할 때 연관된 엔티티들을 함께 조회하려면 FetchType.EAGER
로 설정하거나 페치 조인
을 사용하면 됩니다. 글로벌 fetch 옵션
은 애플리케이션 전체에 영향을 주고 변경할 수 없는 단점이 있습니다. 그래서 일반적으로 글로벌 fetch 옵션 LAZY
를 사용하고, 엔티티를 조회할 때 연관된 엔티티를 함께 조회할 필요가 있으면 JPQL 페치 조인
을 사용합니다.
하지만 페치 조인을 사용하면 같은 JPQL을 중복해서 작성하는 경우가 많습니다.
이러한 단점을 엔티티 그래프
를 사용하여 해결할 수 있습니다. 엔티티 그래프 기능을 사용하면 엔티티를 조회하는 시점에 함께 조회할 연관된 엔티티를 선택할 수 있습니다.
엔티티 그래프의 기능은 엔티티 조회시점에 연관된 엔티티들을 함께 조회하는 기능입니다.
@NamedEntityGraph(name = "Order.withMember", attributeNodes = {
@NamedAttributeNode("member")
})
@Table(name = "ORDERS")
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
private int orderAmount;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
}
위에서 Order-Meber
를 Lazy
로 설정하였지만 EntityGraph
에서 함께 조회할 속성으로 member
를 선택했으므로 이 엔티티 그래프를 사용하면 Order
를 조회할 때 연관된 member
도 함께 조회할 수 있습니다. 둘 이상 정의하려면 @NamedEntityGraphs
를 사용하면 됩니다.
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member(26, "규니");
em.persist(member);
em.flush();
em.clear();
Order order = new Order(50, member);
em.persist(order);
em.flush();
em.clear();
EntityGraph<?> entityGraph = em.getEntityGraph("Order.withMember");
Map hints = new HashMap<>();
hints.put("javax.persistence.fetchgraph", entityGraph);
Order findOrder = em.find(Order.class, 1L, hints);
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
위처럼 getEntityGraph()
에서 EntityGraph
에 지정한 이름으로 조회한 후에 hints
와 함께 Order
를 조회하면 LAZY
로 설정되어 있어도 EntityGraph
가 적용되어 한번에 같이 가져오게 됩니다.
Order
: Item
의 관계는 N : M
이기 때문에 중간에 OrderItem
이라는 매핑 테이블이 존재합니다. 그래서 이번에는 Order -> OrderItem -> Item
까지 한번에 조회해보겠습니다.
그런데 Order -> OrderItem
은 Order가 관리하는 필드지만, OrderItem -> Item
은 Order
가 관리하는 필드가 아닙니다. 이러한 경우에 subgraph
를 사용하면 됩니다.