OrderItem과 Item은 ManyToOne 관계이다.
Item 밑에 서브클랙스 Book, Album, Movie 가 있다.
ParentChildProxyTest에서 프록시를 부모 타입으로 조회하면 부모의 타입을 기반으로 프록시가 생성되는 문제를 살펴보고, 해결방법 실습을 해본다. (책에는 3개만 실습해본다.)
해결방법
JPQL로 대상 직접 조회
프록시 벗기기 (실습)
기능을 위한 별도 인터페이스 제공 (실습)
비지터 패턴 사용 (실습)
1 기본 엔티티들 코드
3. 해결방법에서 엔티티들을 수정해 갈 것이다.
2 테스트 코드 - 문제점 확인
3. 해결방법
1. 프록시 벗기기 (실습)
Hibernate.unproxy()
OrderItem-item 관계에 cascade = CascadeType.PERSIST 추가.
generateOrderItem()에서 OrderItem만 영속화하고 DB에 플러시하면 아이템도 같이 들어간다.
@Entity
@Table(name = "ORDER_ITEM")
@Getter
@Setter
public class OrderItem {
@Id @GeneratedValue
@Column(name = "ORDER_ITEM_ID")
private Long id;
// 지연로딩 설정
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ITEM_ID")
private Item item;
}
---------------------------------------------------------
@Getter
@Setter
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
@ToString
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
}
---------------------------------------------------------
@Setter
@Getter
@Entity
@DiscriminatorValue("M")
public class Movie extends Item{
private String director;
private String actor;
}
@Getter @Setter
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
private String artist;
private String etc;
}
@Getter
@Setter
@Entity
@DiscriminatorValue("B")
public class Book extends Item{
private String author;
private String isbn;
}
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class ParentChildProxyTest {
@PersistenceContext
EntityManager em;
@Test
public void 부모타입으로_프록시조회 () {
Book savedBook = new Book();
savedBook.setName("jpabook");
savedBook.setAuthor("kim");
em.persist(savedBook);
em.flush();
em.clear();
// 프록시를 부모 타입으로 조회 - 문제의 시작점...
Item proxyItem = em.getReference(Item.class, savedBook.getId());
if (proxyItem instanceof Book) { // false
System.out.println("proxyitem instanceof Book");
Book book = (Book) proxyItem;
System.out.println("책 저자 = " + book.getAuthor());
} else {
System.out.println("부모 타입 프록시를 자식 타입하고 instanceof 비교할 수 없습니다.");
}
Assert.assertFalse( proxyItem.getClass() == Book.class ); //같지 않기에 성공
Assert.assertFalse( proxyItem instanceof Book );
Assert.assertTrue( proxyItem instanceof Item );
}
}
// 결과
// "부모 타입 프록시를 자식 타입하고 instanceof 비교할 수 없습니다."
// Assert 모두 성공
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinColumn(name = "ITEM_ID")
private Item item;
------------------------------------------------------------------
@PersistenceUnit
EntityManagerFactory emf;
@PersistenceContext
EntityManager em;
@Test
public void 하이버네이트_언프록시 (){
OrderItem orderItem = generateOrderItem();
boolean isLoaded = emf.getPersistenceUnitUtil().isLoaded(orderItem.getItem());
Item item = orderItem.getItem();
Item unProxyitem = (Item) Hibernate.unproxy(item); //unproxy()
if (unProxyitem instanceof Book) { // 전에 실패 했지만 지금은 성공
System.out.println("=============================================");
System.out.println("proxyitem instanceof Book");
Book book = (Book) unProxyitem;
System.out.println( "책 저자 = " + book.getAuthor());
} else {
System.out.println("부모타입프록시를 자식타입하고 instanceof 비교할 수 없습니다.");
}
Assert.assertTrue(item != unProxyitem); // 프록시 != 원본 엔티티 -> 참
}
public OrderItem generateOrderItem(){
Book savedBook = Book.builder().author("kim").name("bookname").build();
OrderItem orderItem = new OrderItem();
orderItem.setItem(savedBook);
// 주문아이템과 책이 같이 저장, CascasdeType
em.persist(orderItem);
em.flush();
em.clear();
return em.find(OrderItem.class, orderItem.getId());
}
------------------------------------------------------------------
// Book 엔티티에 빌더 추가
@Builder
private Book(String name, String author){
super(name);
this.author = author;
}
public interface Titleview {
String getTitle();
}
----------------------------------------------------------------
@Entity
public abstract class Item implements Titleview {
//...
}
----------------------------------------------------------------
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
//...
@Override
public String getTitle() {
return "Book 타이틀을 리턴합니다.";tName()+" 저자:"+author+"]";
}
}
// Movie, Albume도 동일하 getTitle() 추가
----------------------------------------------------------------
@Entity
public class OrderItem {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ITEM_ID")
private Item item;
public void printItem() {
System.out.println(item.getTitle());
}
//...
}
----------------------------------------------------------------
@Test
public void 기능을_위한_별도_인터페이스_제공_2 (){
OrderItem orderItem = generateOrderItem();
orderItem.printItem();
}
//결과
//"Book 타이틀을 리턴합니다."
//1 인터페이스 정의
public interface Visitor {
void visit(Book book);
void visit(Movie movie);
void visit(Album album);
}
----------------------------------------------------------------
//2 Visitor의 구현 클래스
public class PrintVisitor implements Visitor {
@Override
public void visit(Book book) {
System.out.println("Book 을 프린트합니다.");
}
@Override
public void visit(Movie movie) {...}
@Override
public void visit(Album album) {...}
}
public class TitleVisitor implements Visitor {
// 원하는 기능
}
----------------------------------------------------------------
//3 Book, Movie, Album 부모 클래스에 추상 메소드 추가
public abstract class Item {
//...
public abstract void accept(Visitor visitor);
}
----------------------------------------------------------------
//4 서브클래스 오버라이드 추가
//자신(this)을 파라미터로 넘기는 것이 전부
public class Book extends Item {
//..
@Override
public void accept(Visitor visitor) {
visitor.visit(this); //this는 프록시가 아닌 원본
}
}
public class Movie extends Item {
//..
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
public class Album extends Item {
//..
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
----------------------------------------------------------------
@Test
public void 비지터_패턴_사용_3 (){
OrderItem orderItem = generateOrderItem();
Item item = orderItem.getItem();
item.accept(new PrintVisitor());
//프록시를 부모 타입으로 조회하면,
//부모의 타입을 기반으로 프록시가 생성되는 문제를 해결하기 위해
//서브클래스 타입을 확인하기 위한 방법에 대해 테스트해본 것이다.
}
//결과
//Book 을 프린트합니다.