컬렉션
- JPA는 자바에서 기본으로 제공하는 Collection, List, Set, Map 컬렉션을 지원하며
@OneToMany, @ManyToMany를 사용해서 일대다나 다대다 엔티티 관계를 매핑할 때,
@ElementCollection을 사용해서 값 타입을 하나 이상 보관할 때 사용 - 자바 컬렉션 인터페이스의 특징
- Collection : 자바가 제공하는 최상위 컬렉션으로 하이버네이트는 중복을 허용하고 순서를 보장하지 않음
- Set : 중복을 허용하지 않는 컬렉션으로 순서를 보장하지 않음
- List : 순서가 있는 컬렉션으로 순서를 보장하고 중복을 허용
- Map : Key, Value 구조로 되어 있는 특수한 컬렉션
- JPA와 컬렉션
하이버네이트는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 하이버네이트에서 준비한 컬렉션으로 감싸서 사용
하이버네이트는 컬렉션을 효율적으로 관리하기 위해 엔티티를 영속 상태로 만들 때
원본 컬렉션을 감싸고 있는 내장 컬렉션을 생성해서 이 내장 컬렉션을 사용하도록 참조를 변경
이렇게 하이버네이트가 제공하는 내장 컬렉션은 원본 컬렉션을 감싸고 있어서 래퍼 컬렉션으로도 부름
하이버네이트의 이런 특징 때문에 컬렉션을 사용할 때 즉시 초기화해서 사용하는 것을 권장
예) ArrayList 타입이었던 컬렉션이 엔티티를 영속 상태로 만든 직후 하이버네이트가 제공하는 PersistentBage 타입으로 변경
// JPA 컬렉션 사용
@Entity
public class Team {
@Id
private String id;
@OneToMany
@JoinColumn
private Collection<Member> members = new ArrayList<Member>();
...
}
// Team이 members 컬렉션을 필드로 가지고 있을 때 영속 상태로 만들기
Team team = new Team();
// 출력 결과 : before persist = class java.util.ArrayList
System.out.println("before persist = " + team.getMembers().getClass());
em.persist(team);
// 출력 결과 : before persist = class org.hibernate.collection.internal.PersistentBag
System.out.println("after persist = " + team.getMembers().getClass());
// 인터페이스와 컬렉션 래퍼
// 인터페이스에 따라 다른 래퍼 컬렉션 사용
/* --- 컬렉션 인터페이스가 Collection, List일 때는 내장 컬렉션으로 PersitentBag 사용 ---
PersistentBag는 중복을 허용하고 순서를 보장하지 않음 */
@OneToMany
Collection<Member> collection = new ArrayList<Member>();
@OneToMany
List<Member> list = new ArrayList<Member>();
/* --- 컬렉션 인터페이스가 Set일 때는 내장 컬렉션으로 PersitentSet 사용 ---
PersistentSet는 중복을 허용하지 않고 순서를 보장하지 않음 */
@OneToMany
Set<Member> setl = new HashSet<Member>();
/* --- 컬렉션 인터페이스가 List + @OrderColumn 일 때는 내장 컬렉션으로 PersitentList 사용 ---
PersistentList는 중복을 허용하고 순서를 보장함 */
@OneToMany @OrderColumn
List<Member> orderColumnList = new ArrayList<Member>();
- Colletion, List
Collection, List 인터페이스는 중복을 허용하는 컬렉션이고 PersistentBag을 래퍼 컬렉션으로 사용하며 ArrayList로 초기화
중복을 허용한다고 가정하므로 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고 단순히 저장만 하면 됨
따라서 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화하지 않음
또한 객체를 추가하는 add() 메소드는 내부에서 어떤 비교도 하지 않고 항상 true를 반환하며
같은 엔티티가 있는지 찾거나 삭제할 때는 equals() 메소드를 사용
// Collection, List 예제
@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>();
...
}
List<Comment> comments = new ArrayList<Comment>();
...
// 단순히 추가만 하며 결과는 항상 true
boolean result = comments.add(date)
comments.contains(comment); // equals 비교
comments.remove(comment); // equals 비교
- Set
Set은 중복을 허용하지 않는 컬렉션이고 하이버네이트는 PersistentSet을 컬렉션 래퍼로 사용하며 HashSet으로 초기화
Set은 엔티티를 추가할 때 중복된 엔티티가 있는지 비교해야 하므로 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화 함
또한 HashSet은 중복을 허용하지 않으므로 add() 메소드로 객체를 추가할 때마다 equals() 메소드로 같은 객체가 있는지 비교
같은 객체가 없으면 객체를 추가하고 true를 반환하고, 같은 객체가 이미 있어서 추가에 실패하면 false를 반환
참고로 HashSset은 해시 알고리즘을 사용하므로 hashcode()도 힘께 사용해서 비교
// Set 예제
@Entity
public class Parent {
@OneToMany
@JoinColumn
private Set<SetChild> set = new HashSet<SetChild>();
...
}
Set<Comment> comments = new HashSet<Commet>();
...
boolean result = comments.add(date); // hashcode + equals 비교
comments.contains(comment); // hashcode + equals 비교
comments.remove(comment); // hashcode + equals 비교
- List + @OrderColumn
List 엔티티에 @OrderColumn을 추가하면 순서가 있는 특수한 컬렉션으로 인식함
순서가 있다는 의미는 데이터베이스에 순서 값을 저장해서 조회할 때 사용한다는 의미이며 PersistentList를 사용
순서가 있는 컬렉션은 데이터베이스에 순서 값도 함께 관리함
예) @OrderColumn의 name 속성에 POSITION이라는 값을 주었을 때
JPA는 List의 위치 값을 테이블의 POSITION 컬럼에 보관하며
Board.comments 컬렉션은 Board 엔티티에 있지만 테이블의 일대다 관계의 특성상,
위치 값인 POSITION 컬럼은 COMMENT 테이블에 매핑됨
하지만 실무에서 사용하기에는 아래의 단점이 존재하므로 직접 POSITON 값을 관리하거나 @OrderBy를 사용하길 권장
- @OrderColumn을 Board 엔티티에 매핑하므로 Comment는 POSITON의 값을 알 수 없음
그래서 Comment를 INSERT할 때는 POSITION 값이 저장되지 않지만
POSITION은 Board.comments의 위치 값이므로, 이 값을 사용해서 POSITION의 값을 UPDATE하는 SQL이 추가 발생 - List를 변경하면 연관된 많은 위치 값을 변경해야 함
예) 댓글 2를 삭제하면 댓글 3, 4의 POSITION 값을 하나씩 줄이는 UPDATE SQL이 2번 추가로 실행됨 - 중간에 POSITION 값이 없으면 조회한 List에는 null이 보과됨
예) 댓글 2를 데이터베이스에서 강제로 삭제하고 다른 댓글들의 POSITION 값을 수정하지 않으면
데이터베이스의 POSITION 값은 [0, 2, 3]이 되어서 중간에 1 값이 없어 List를 조회하면 1번 위치에 null이 보관되고
컬렉션을 순회할 때 NullPointerException이 발생
- @OrderColumn을 Board 엔티티에 매핑하므로 Comment는 POSITON의 값을 알 수 없음
// List + @OrderColumn 예제
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
private String content;
@OneToMany(mapped = "board")
@OrderColumn(name = "POSITION")
// List 인터페이스 사용 + @OrderColumn을 추가
// Board.comments는 순서가 있는 컬렉션으로 인식
private List<Commnet> comments = new ArrayList<Comment>();
...
}
@Entity
public class Comment {
@Id @GanaratedValue
private Long id;
private String comment;
@ManyToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
...
}
// List 컬렉션은 내부에 위치 값을 가지고 있으므로 List의 위치 값을 활용할 수 있음
list.add(1, data1); // 1번 위치에 data1을 저장해라
list.get(10); // 10번 위치에 있는 값을 조회해라
// @OrderColumn을 사용하는 예
Board board = new Board("제목1", "내용1");
em.persist(board);
Comment comment1 = new Comment("댓글1");
comment1.setBord(board);
board.getComments().add(comment1); // POSITION 0
em.persist(comment1);
Comment comment2 = new Comment("댓글2");
comment2.setBord(board);
board.getComments().add(comment2); // POSITION 1
em.persist(comment2);
Comment comment3 = new Comment("댓글3");
comment3.setBord(board);
board.getComments().add(comment3); // POSITION 2
em.persist(comment3);
Comment comment4 = new Comment("댓글4");
comment4.setBord(board);
board.getComments().add(comment4); // POSITION 3
em.persist(comment4);
- @OrderBy
데이터베이스의 ORDER BY절을 사용해서 컬렉션을 정렬하므로 순서용 컬럼을 매핑하지 않아도 됨
그리고 @OrderBy는 모든 컬렉션에 사용할 수 있으며 @OrderBy의 값은 엔티티의 필드를 대상으로 함
// @OrderBy 예제
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
// @OrderBy를 적용하고 Member의 username 필드로 내림차순 정렬하고 id로 오름차순 정렬
@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;
...
}
// Team.members를 초기화할 때 실행된 SQL
SELECT M.*
FROM
MEMBER M
WHERE
M.TEAM_ID=?
ORDER BY
M.MEMBER_NAME DESC,
M.ID ASC
@Converter
- 컨버터를 사용하면 엔티티의 데이터를 변환해서 데이터베이스에 저장할 수 있음
예) 회원의 VIP 여부를 자바의 boolean 타입을 사용하고 싶을 때
데이터베이스에 숫자(0, 1) 대신 문자 Y 또는 N으로 저장하고 싶다면 컨버터를 사용
// 매핑할 테이블
CREATE TABLE MEMBER {
ID VARCHAR(255) NOT NULL,
USERNAME VARCHAR(255),
VIP VARCHAR(1) NOT NULL, // 문자 Y, N을 입력하려고 1로 지정
PRIMARY KEY (ID)
}
// 회원 엔티티
@Entity
public class Member {
@Id
private String id;
private String username;
// @Convert를 적용해서 데이터베이스에 저장되기 직전에 BooleanToYNConverter가 동작
@Convert(converter=BooleanToYNConverter.class)
private boolean vip;
// Getter, Setter
...
}
// Boolean을 YN으로 변환해주는 컨버터
/* @Converter 어노테이션을 사용하고 AttributeConverter 인터페이스를 구현한 후
제네릭에 현재 타입과 반환할 타입을 지정해야 함
여기서는 Boolean 타입을 String 타입으로 변환함 */
@Converter
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
public interface AttributeConverter<X, Y> {
// 엔티티의 데이터를 데이터베이스 컬럼에 저장할 데이터로 변환 (true -> Y, false -> N)
public Y convertToDatabaseColumn(X attribute);
// 데이터베이스에 조회한 컬럼 데이터를 엔티티의 데이터로 변환 (Y -> true, N -> false)
public X convertToEntityAttribute(Y dbData);
}
// 컨버터 클래스 레벨에 설정하기
@Entity
// 어떤 필드에 컨버터를 적용할지 명시
@Convert(converter=BooleanToNYConverter.class, attributeName = "vip")
public class Member {
@Id
private String id;
private String username;
private boolean vip;
...
}
- 글로벌 설정
모든 Boolean 타입에 컨버터를 적용하려면 @Converter(autoApply = true) 옵션을 적용
@Convert 속성 정리
- converter : 사용할 컨버터를 지정
- attrubuteName : 컨버터를 적용할 필드를 지정
- disableConversion : 글로벌 컨버터나 상속 받은 컨버터를 사용하지 않으며 기본값은 false
// 컨버터 글로벌 설정
@Converter(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를 지정하지 않아도 모든 Boolean 타입에 자동으로 컨버터 적용
@Entity
public class Member {
@Id
private String id;
private String username;
private boolean vip;
// Getter, Setter
...
}
리스너
- 모든 엔티티를 대상으로 언제 어떤 사용자가 삭제를 요청했는지 모두 로그로 남겨야 하는 요구사항이 있다면
애플리케이션 삭제 로직을 하나씩 찾아서 로그를 남기는 것은 비효율적이므로
JPA 리스너 기능을 사용해 엔티티의 생명주기에 따른 이벤트를 처리
이벤트를 잘 활용하면 대부분의 엔티티에 공통으로 적용하는 등록 일자, 수정 일자 처리와
해당 엔티티를 누가 등록하고 수정했는지에 대한 기록을 리스너 하나로 처리할 수 있음 - 이벤트 종류
- PostLoad
엔티티가 영속성 컨텍스트에 조회된 직후 또는 refresh를 호출한 후 - PrePersist
persist() 메소드를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에 호출
식별자 생성 전략을 사용한 경우 엔티티에 식별자는 아직 존재하지 않으며 새로운 인스턴스를 merge할 때도 수행됨 - PreUpdate
flush나 commt을 호출해서 엔티티를 데이터베이스에 수정하기 직전에 호출 - PreRemove
remove() 메소드를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출됨
또한 삭제 명령어로 영속성 전이가 일어날 때도 호출되며, orphanRemobal에 대해서는 flush나 commit 시에 호출됨 - PostPersist
flush나 commit을 호출해서 엔티티를 데이터베이스에 저장한 직후에 호출되며 식별자가 항상 존재
참고로 식별자 생성 전략이 IDENTITY면 식별자를 생성하기 위해 persist()를 호출하면서
데이터베이스에 해당 엔티티를 저장하므로 이때는 persist()를 호출한 직후에 바로 PostPersist가 호출됨 - PostUpdate
flush나 commit을 호출해서 엔티티를 데이터베이스에 수정한 직후에 호출됨 - PostRemove
flush나 commit을 호출해서 엔티티를 데이터베이스에 삭제한 직후에 호출됨
- PostLoad
- 이벤트 적용 위치
이벤트는 엔티티에서 직접 받거나 별도의 리스너를 등록해서 받을 수 있음
- 엔티티에 직접 적용
엔티티에 이벤트가 발생할 때마다 어노테이션으로 지정한 메소드가 실행됨 - 별도의 리스너 등록
이벤트를 처리할 별도의 리스너를 등록하는 방법이며
리스너는 대상 엔티티를 파라미터로 받을 수 있고 반환 타입은 void로 설정해야 함 - 기본 리스너 사용
모든 엔티티의 이벤트를 처리하려면 META-INT/orm.xml에 기본 리스너로 등록
여러 리스너를 등록했을 때 이벤트 호출 순서는 기본 리스터 → 부모 클래스 리스너 → 리스너 → 엔티티 - 더 세밀한 설정을 위한 어노테이션
ExcludeDefaultListeners : 기본 리스너 무시
ExcludeSuperclassListeners : 상위 클래스 이벤트 리스너 무시
- 엔티티에 직접 적용
// 엔티티에 직접 적용
@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");
}
...
}
// 엔티티를 저장하면 메소드 실행하여 출력
Duck.prePersist id=null (아이디가 생성되기 전에 호출됨)
Duck.postPersist id=1 (아이디가 생성된 후에 호출됨)
// 별도의 리스너 등록
@Entity
@EntityListeners(DuckListener.class)
public class Duck {
...
}
public class DuckLister {
@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 + "]");
}
{
// 기본 리스너 사용
// META-INF/orm.xml
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings ...>
<persistence-unit-metadata>
<persistence-unit-defaults>
<entity-listeners>
<entity-listener class="jpabook.jpashop.domain.test.listener.DefaultListener" />
</entity-listeners>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>
// 더 세밀한 설정을 위한 어노테이션
@Entity
@EntityListeners(DuckListener.class)
@ExcludeDefaultListeners
@ExcludeSuperclassListeners
public class Duck extends BaseEntity {
...
}
엔티티 그래프
- 엔티티를 조회할 때 연관된 엔티티들을 함께 조회하려면
글로벌 fetch 옵션을 FetchType.EAGER로 설정하거나 JPQL에서 페치 조인을 사용하면 됨
하지만 글로벌 fetch 옵션은 애플리케이션 전체에 영향을 주고 변경할 수 없는 단점이 존재하므로
일반적으로 글로벌 fetch 옵션은 FetchType.LAZY를 사용하고
엔티티를 조회할 때 연관된 엔티티를 함께 조회할 필요가 있으면 JPQL의 페치 조인을 사용
하지만 페치 조인을 사용하면 함께 조회할 엔티티에 따라 다른 JPQL을 사용해야 하므로 같은 JPQL을 중복 작성하는 경우 발생
이는 JPQL이 데이터를 조회하는 기능뿐만 아니라 연관된 엔티티를 함께 조회하는 기능도 제공하기 때문에 발생하는 것
그러므로 엔티티 그래프 기능을 사용하면 엔티티를 조회하는 시점에 함께 조회할 연관된 엔티티를 선택할 수 있음
따라서 JPQL은 데이터를 조회하는 기능만 수행하면 되고 연관된 엔티티를 함께 조회하는 기능이 엔티티 그래프를 사용하면 됨
엔티티 그래프는 정적으로 정의하는 Named 엔티티 그래프와 동적으로 정의하는 엔티티 그래프가 존재
- Named 엔티티 그래프
주문(Order)을 조회할 때 연관된 회원(Member)도 함께 조회하는 엔티티 그래프
// 엔티티 그래프 예제
/* Named 엔티티 그래프는 @NamedEntityGraph로 정의
name : 엔티티 그래프의 이름을 정의
attributeNodoes : @NamedAttributeNode를 사용하고 그 값으로 함께 조회할 속성을 선택
둘 이상 정의하려면 @NamedEntityGraphs를 사용 */
@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가 지연 로딩으로 설정되어 있지만,
엔티티 그래프에서 함께 조회할 속성으로 member를 선택했으므로
이 엔티티 그래프를 사용하면 Order를 조회할 때 연관된 member도 함께 조회 */
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "MEMBER_ID")
private Member member; // 주문 회원
...
}
- em.find()에서 엔티티 그래프 사용
Named 엔티티 그래프를 사용하려면 em.getEntityGraph("Order.withMember")를 통해 엔티티 그래프를 찾아오면 됨
엔티티 그래프는 JPA의 힌트 기능을 사용해서 동작하는데
힌트의 키로 javax.persistence.fetchgraph를 사용하고 힌트의 값으로 찾아온 엔티티 그래프를 사용하면 됨
em.find(Order.class, orderId, hints)로 Order 엔티티를 조회할 때 힌트 정보도 포함하므로
실행된 SQL을 보면 적용한 Order.withMember 엔티티 그래프를 사용해서 Order와 Member를 함께 조회
// 엔티티 그래프 사용
EntityGraph graph = em.getEntityGraph("Order.withMember");
Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);
Order order = em.find(Order.class, orderId, hints);
// 실행된 SQL
select o.*, m.*
from
ORDERS o
inner join
MEMBER m
on o.MEMBER_ID=m.MEMBER_ID
where
o.ORDER_ID=?
- subgraph
Order → OrderItem → Item까지 함께 조회하려고 할 때, Order → OrderItem은 Order가 관리하는 필드지만
OrderItem → Item은 Order가 관리하는 필드가 아니므로 subgraph 사용
// subgraph
/* Order.withAll이라는 Named 엔티티 그래프를 정의하며
이 엔티티 그래프는 Order -> Member, Order -> OrderItem, OrderItem -> Item의 객체 그래프를 함께 조회
이 때 OrderItem -> Item은 Order의 객체 그래프가 아니므로 subgraph 속성으로 정의해야 함
이를 위해 @NamedSubgraph를 사용해서 서브 그래프를 적용하여
orderItems라는 이름의 서브 그래프가 item을 함께 조회하도록 정의 */
@NamedEntityGraph(name = "Order.withAll", attributeNodes = {
@NamedAttributeNode("member"),
@NamedAttributeNode(value = "orderItems", subgraph = "orderItems")
},
subgraphs = @NamedSubgraph(name = "orderItems", attributeNodes = {
@NamedAttributeNode("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; // 주문 상품
...
}
// 사용하는 코드
// Order.withAll이라는 Named 엔티티 그래프를 사용해서 Order 엔티티와 엔티티 그래프에서 지정한 엔티티를 조회
Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", em.getEntityGraph("Order.withAll"));
Order order = em.find(Order.class, orderId, hints);
// 실행된 SQL
select o.*, m.*, oi.*, i.*
from
ORDERS o
inner join
Member m
on o.MEMBER_ID=m.MEMBER_ID
inner 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에서 엔티티 그래프 사용
JPQL에서 엔티티 그래프를 사용하는 방법은 em.find()와 동일하게 힌트만 추가하면 됨
// JPQL에서 엔티티 그래프 힌트
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();
// 실행된 SQL
select o.*, m.*, oi.*, i.*
from
ORDERS o
left outer 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=?
- 동적 엔티티 그래프
엔티티 그래프를 동적으로 구성하려면 createEntityGraph() 메소드를 사용하면 됨
// 동적 엔티티 그래프 - Named 엔티티 그래프
/* em.createEntityGraph(Order.class)를 사용해서 동적으로 엔티티 그래프를 만든 후
graph.addAttributeNodes("member")를 사용해서 Order.member 속성을 엔티티 그래프에 포함 */
EntityGraph<Order> graph = em.createEntityGraph(Order.class);
graph.addAttributeNodes("member");
Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);
Order order = em.find(Order.class, orderId, hints);
// 동적 엔티티 그래프 - subgraph
/* graph.addSubgraph("orderItems") 메소드를 사용해서 서브 그래프를 만든 후
서브 그래프가 item 속성을 포함하도록 함 */
EntityGraph<Order> graph = em.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 = em.find(Order.class, orderId, hints);
- 엔티티 그래프 주의사항
- 엔티티 그래프는 항상 조회하는 엔티티의 ROOT에서 시작해야 함
- 영속성 컨텍스트에 해당 엔티티가 이미 로딩되어 있으면 엔티티 그래프가 적용되지 않음
하지만 아직 초기화되지 않은 프록시에는 엔티티 그래프가 적용됨 - javax.persistence.fetchgraph 힌트를 사용하면 엔티티 그래프에 선택한 속성만 함께 조회하는 반면에
javax.persistence.loadgraph 속성은 엔티티 그래프에 선택한 속성뿐만 아니라
글로벌 fetch 모드가 FetchType.EAGER로 설정된 연관관계도 포함해서 함께 조회
'Java-Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 고급 주제와 성능 최적화 ② (0) | 2022.06.07 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 고급 주제와 성능 최적화 ① (0) | 2022.06.04 |
[자바 ORM 표준 JPA 프로그래밍] 웹 애플리케이션과 영속성 관리 (0) | 2022.05.26 |
[자바 ORM 표준 JPA 프로그래밍] 스프링 데이터 JPA ③ (0) | 2022.05.18 |
[자바 ORM 표준 JPA 프로그래밍] 스프링 데이터 JPA ② (0) | 2022.05.17 |