em.find()
을 사용하면, 그 순간 DB 호출 함수를 실행하고, 해당 값을 반환해준다.
그런데, jpa에서는 em.getReference()
라는 함수를 지원해준다.
Member findMember = em.find(Member.class, member.getId());
// System.out.println("findMember.getId() = " + findMember.getId());
// System.out.println("findMember.getUsername() = " + findMember.getUsername());
위 코드에서는 findMember의 그 어떤 데이터들도 활용이 되지 않지만, DB 호출 함수는 실행이 되고, findMember에는 그 값이 들어가 있다. 아래는 getReference()
사용 예제이다.
Member findMember = em.getReference(Member.class, member.getId());
// System.out.println("findMember.getId() = " + findMember.getId());
// System.out.println("findMember.getUsername() = " + findMember.getUsername());
위 코드에서 우리는 findMember의 그 어떤 데이터들도 활용하지 않는다. 이때 EntityManager는 DB 호출 함수를 실행하지 않는다.
그럼 findMember에 반환되는 값은 대체 무엇인가!
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
이 함수를 실행해보면
findMember = class org.example.Member$HibernateProxy$VZBUc9hc
이와 같은 결과가 나오는데, Member는 맞는데 뒤에 Hibernate(jpa 구현체)의 Proxy 라고 나와있다.
즉 Hibernate 가 만들어낸 가짜 클래스 라는 뜻이다.
@Entity 어노테이션이 걸려있는 class가 있고, Proxy라는 객체가 이 Entity를 상속받는다.
(타입 비교시 조심할 것!!, 덧. 이미 영속성컨텍스트에 올라와 있으면, Entity를 바로 반환)
Proxy안에는 Entity target 이 있음.
[ 호출 사용시 ]
1. Proxy에서 실제 데이터 요청이 들어온다.
2. Proxy는 영속성 컨텍스트에 요청을 한다. (초기화 요청)
3. 영속성 컨텍스트는 DB 호출 함수를 실행한다.
4. 영속성 컨텍스트는 실제 Entity를 생성한다.
5. 그 Entity의 값을 1.의 데이터 요청에 반환한다.
덧. find(), reference()를 같이 쓰게 되는 경우.
같은 id 값으로 조회가 되는 것이라면, 둘다 같은 type으로 반환 됨.
둘다 Entity 거나, 둘다 Proxy거나!!
(+) 준영속, 비영속 상태가 되면, proxy 초기화는 불가능해짐!
해당 프록시가 초기화가 되었는지 아닌지를 확인할 수 있다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
...
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(findMember)); // false
System.out.println("findMember.getUsername() = " + findMember.getUsername());
System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(findMember)); // true
...
EntityMangerFactory
의 함수인 getPersistenceUnitUtil().isLoaded()
을 활용하여 proxy의 초기화 여부를 확인 할 수 있음.
프록시 내부 메소드를 사용하여 초기화를 하는 것이 아닌, 직접 초기화 함수 호출
Member findMember = em.getReference(Member.class, member.getId());
Hibernate.initialize(findMember); // 프록시 강제 초기화
(덧) jpa 표준엔 강제호출은 없음. hibernate가 제공해주는 것임!
<필요성>
흠... Member는 Team을 가지고 있기는 한데, 모든 Member가 불러질때 꼭 Team을 받을 필요는 없는데... 그렇다고 필요할때마다 Team을 호출하는 함수를 만들기도 귀찮고... 좋은 방법 없을까?
아! 그럼 Team을 프록시로 설정 하면 되겠구나!!
Member.java (`ManyToOne(fetch = FetchType.LAZY)
추가)
@Entity
@Table(name = "MEMBER")
public class Member{
@Id @GeneratedValue
@Column(name = "MEMEBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
Team.java
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
@Column(name = "TEAMNAME")
private String name;
}
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.getUsername() = " + findMember.getUsername());
// 이때까지도, Team 데이터 호출하지 않음. 이때 team은 proxy객체로 저장되어 있음
System.out.println("findMember Team Class = " + findMember.getTeam().getClass()); // Team$HibernateProxy$prypi1Yf
System.out.println("findMember.getTeamName = " + findMember.getTeam().getName()); // Team 호출
즉
@ManyToOne(fetch = FetchType.LAZY)
를 활용하면,
해당 Column 의 변수는 Proxy 로 가져온다는 것을 알 수 있음!!
흠 근데 사실... Member를 부르고, Team을 사용하는게 전체 90% 정도는 되는걸? 거의 왠만하면 같이 쓰는데... 그냥 Member 부를때 다 땡기자!
Member.java team에 @ManyToOne(fetch = FetchType.EAGER)
를 추가하면 된다.
이렇게 하면 getClass()
하면 Proxy가 나오던 지연 로딩과는 다르게, 바로 Entity class로 나오는 것을 알 수 있다!!
김영한 강사님 추천!!
가급적이면 모든 연관관계를 지연로딩으로 사용해라.
즉시로딩을 적용하면 예상하지 못한 SQL 이 발생함!
즉시로딩은 JPQL 에서 N + 1 문제를 일으킨다.
==> 상상치 못한 쿼리가 날아감!!
@ManyToOne, @OneToOne
기본이 즉시 로딩 -> LAZY 로 설정할 것!!
@OneToMany, @ManyToMany
기본이 지연 로딩
추후fetch join
이라고, 화면별로 필요하게 같이 가져올 수 있도록 설정 가능!
지금껏 배운 영속성 컨텍스트에 persist 하는 방법으로 이런 식으로 가능하다.
그런데 이쯤 되면, 뭔가 불편하지 않은가?
Parent parent = new Parent("부모");
Child child1 = new Child("child1");
Child child2 = new Child("child2");
Child child3 = new Child("child3");
parent.addChild(child1);
parent.addChild(child2);
parent.addChild(child3);
em.persist(parent);
em.persist(child1);
em.persist(child2);
em.persist(child3);
parent 하나만 등록하면 child 는 좀 알잘딱깔쎈 안되나?
아래와 같이 CascadeType.ALL
을 넣어주면,
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
Parent parent = new Parent("부모");
Child child1 = new Child("child1");
Child child2 = new Child("child2");
Child child3 = new Child("child3");
parent.addChild(child1);
parent.addChild(child2);
parent.addChild(child3);
em.persist(parent); // child 들은 persist 가 자동으로 됨!
// em.persist(child1);
// em.persist(child2);
// em.persist(child3);
없애는 경우에도, child 같이 지워짐!
em.remove(parent);
그저 편의성을 위한 것임. 연관관계 매핑하고는 일체 상관 없음!!!!
하나의 부모가 자식들을 관리할때는 매우 유용! 다른 경우는 선택적 사용!
- 단일 소유자 일때
- 라이프사이클이 유사할때
부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동 삭제!
orphanRemoval = true
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(1); // 실제 Delete 함수 호출!
해당 옵션을 넣지 않으면, remove 함수는 그저 객체 단에서만 작동하게 된다.
- 참조하는 곳이 하나 일때 사용
- 특정 엔티티가 개인 소유할 때 사용
@OneToOne, @OneToMany
에서만 사용 가능
CascadType.ALL + orphanRemovel=true
- 스스로 생명주기를 관리하는 엔티티는
em.persist()
로 영속화,em.remove()
로 제거- 부모 엔티티를 통해서 자식의 생명주기를 관리 할 수 있음
- 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용