프록시
Member findMember = em.getReference(Member.class, member.getId());
//실제 객체 사용시 select쿼리 생성
System.out.println("findMember.getId() = " + findMember.getId());
System.out.println("findMember.getUsername() = " + findMember.getUsername());
위에서 생성한 객체인 findMember의 정체는 무엇일까?
System.out.println("findMember = " + findMember.getClass());
//출력값
findMember = class hellojpa.Member$HibernateProxy$VLw384CP
메서드에서 매개변수가 프록시로 넘어올지 실제 값으로 넘어올지 모르기 때문에 == 비교를 하면 안된다
-> (ex) m1 instanceof Member 사용하기
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());
System.out.println(m1.getClass());
System.out.println(m2.getClass());
System.out.println("m1 == m2: " + (m1.getClass() == m2.getClass()));
//출력값
class hellojpa.Member
class hellojpa.Member$HibernateProxy$FvgZWIDg
m1 == m2: false
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush(); //영속성 컨텍스트가 보관하고 있는 캐시된 쿼리를 해소
//flush는 persist 시에 자동으로 발생한다, 명시적으로 비었음을 강조하기 위해 호출한 것 뿐
em.clear(); //1차 캐시된 객체들을 clear로 정리하기 위함
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 = " + m1.getClass());
Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference = " + reference.getClass());
//출력값
m1 = class hellojpa.Member
reference = class hellojpa.Member
위 코드 풀이 : 영속성 컨텍스트에 이미 찾는 엔티티가 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다.
1. 이미 멤버를 영속성 컨텍스트에 올려놓았기 때문에 굳이 프록시를 가져오지 않고 원본을 가져오는 것이 이득이다.
2. == 비교 : JPA는 한 영속성 컨텍스트 안에서 가져온것이고, PK값이 똑같으면 항상 true 값을 반환해야 한다.
2번을 코드로 풀면
System.out.println("refMember == findMember: " + (refMember == findMember));
//위 출력값이 항상 true가 나와야 한다 (같은 영속성 컨텍스트에 속해있을 경우)
//따라서
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember = " + refMember.getClass());
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember = " + findMember.getClass());
//getReference 로 프록시를 먼저 호출한 후 find를 해도 프록시 값이 나오게된다
//(find를 먼저하고 getReference를 하면 프록시가 아닌 본래의 값이 나온다)
영속성 컨텍스트의 도움을 받지 못하는 준영속 상태일 경우, 프록시를 초기화하면 문제가 발생한다
=> 실무에서 자주 겪을 수 있다
em.detach(refMember)
//em.clear()
//em.close() 이후
refMember.getUsername(); // 조회 시
org.hibernate.LazyInitializationException //예외 발생
즉시 로딩과 지연 로딩
멤버만을 조회하고 싶은데 조인관계로 인해 팀까지 조회하는 경우가 발생할 수 있다
=> "지연로딩" 이라는 옵션을 제공
지연로딩을 세팅하는 방법
public class Member{
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private Team team;
...
}
위처럼 함으로써 멤버를 조회할 경우 팀에대한 값을 프록시 값으로 가져온다.
이후 팀을 직접 사용할 때 초기화한다.
public class Member{
...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn
private Team team;
...
}
즉시로딩을 하면 em.find(Member.class, ... )를 호출할 때 프록시 값이 아닌 진짜 값을 가져온다.
프록시와 즉시로딩 사용시 주의
• Member와 Team은 자주 함께 사용 -> 즉시 로딩
• Member와 Order는 가끔 사용 -> 지연 로딩
• Order와 Product는 자주 함께 사용 -> 즉시 로딩
이후 프록시인 orders를 사용하게 되면 EAGER로 걸려있는 product는 프록시값이 아닌 본래의 값이 호출된다.
영속성 전이(CASCADE)와 고아 객체
영속성 전이
em.persist(parent);
em.persist(child1);
em.persist(child2);
위 코드처럼 영속성을 각각 등록하지 않고,
parent만을 건드려서 child 또한 영속성 컨텍스트에 추가하고 싶은 경우
@OneToMany(mappedBy = "parent",cascade = CascadeType.ALL)
그렇다면 OneToMany 관계에서는 모두 cascade 옵션을 추가 해야되나?
= X
만약, child가 부모인 parent 클래스 외에 다른 클래스와도 관계가 있는경우에는
parent 클래스에 cascade를 해주지 않는다.
(운영이 너무 힘들어지기 때문)
즉, 단일 엔티티에 종속적인 경우에만 사용하자 (parent와 child의 life cycle이 동일할 경우)
고아 객체
부모 객체가 제거되면 그의 자식 객체도 같이 제거가 된다.
=> 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 판단하고 삭제하는 기능
cascade와 마찬가지로 아래의 조건 모두 만족할 때 사용한다
@OneToMany, @OneToOne 만 가능하다
(부모 엔티티의 자식 엔티티를 자동으로 지우는 것이니 당연히 OneToXXX 만 가능)
Cascade 정리
1. 완전 개인 소유인 경우에만 사용할 수 있다 (게시판 <-> 첨부파일)
2. DDD(도메인 주도 설계)의 Aggregate Root와 어울린다
3. 애매하면 사용하지 않는다(Order <-> Delivery)
CascadeType.ALL + orphanRemove=true 정리
3.. orphanRemoval이 CascadeType.All or CascadeType.PERSIST 인 경우 정상적으로 DELETE 쿼리가 생성된다
=> 실무에서는 orphanRemoval만 따로 적용하는 경우도 많지 않을 뿐더러, 주인 엔티티가 하위 엔티티를 관리하는 경우에는 CascadeType.PERSIST + orphanRemoval 을 함께 적용하기 때문에 영향이 크지 않다.
실전 예제 5 - 연관관계 관리
Reference
김영한 님 - 자바 ORM 표준 JPA 프로그래밍 - 기본편