Member member = new Member();
member.setName("hello");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.name = " + findMember.getName());
class Member {
private int id;
private String name;
}
class MemberProxy extends Member { // 실제 Member 클래스를 상속받음
private Member target; // 실제 엔티티 객체 참조
// private Long id = 1L; // ID 값은 초기화됨
@Override
public String getName() {
if (target == null) { initialize(); }
return target.getName();
}
}
참고로,
- JPA는 em.getReference(Member.class, memger.getId());를 할때 이미 id값이 무엇인지 알고 있기 때문에
em.getReference(Member.class, id) 호출 시 JPA가 프록시 객체를 생성하면서 내부적으로 ID필드를 초기화한다.
프록시 객체는 PK인 id만 초기화되어있다. 나머지 필드는 초기화되어있지않다.
그래서 findMember.getId()을 하더라도 이미 id값이 프록시 객체에 초기화되어있기 때문에 db조회 없이 id값을 반환할 수 있다.- getName()을 하면 실제객체의 getName()을 호출하게되고, getId()를 하면 프록시객체의 getId()를 호출한다.
- member.getId()의 값과 findMember.getId()의 값은 무조건 동일하다. 즉 동일한 PK이다.
그리고, em.getReference()를 하게되면, 프록시 객체가 영속성컨텍스트(1차캐시)에 저장된다. 초기화를 해도 실제 객체는 1차캐시에 저장되지 않는다. 프록시 객체가 실제 객체의 참조를 가지고있기 때문에 프록시 객체를 통해 실제 객체를 사용할 수 있다. 즉 실제 객체가 1차캐시에 등록되는건 아니고 프록시 객체가 실제 객체의 참조를 가지고 있기 때문에 실제 객체가 1차 캐시에 등록되어 있는 것처럼 사용할 수 있다.
프록시 객체와 실제객체의 타입이 다르다.
== 비교 : 단순히 클래스타입을 비교한다. 실제객체의 타입과 프록시객체의 타입이 다름.
Member member1 = em.find(Member.class, 1L); // 실제 객체
Member member2 = em.getReference(Member.class, 2L); // 프록시 객체
System.out.println("member1.getClass() = " + member1.getClass()); // class hellojpa.Member
System.out.println("member2.getClass() = " + member2.getClass()); // class hellojpa.Member$HibernateProxy$Dhz1jRg4
System.out.println(member1.getClass() == member2.getClass()); // false
instanceof : 해당 객체의 타입이 특정 클래스타입이나 그 하위 타입인지 검사
Member member1 = em.find(Member.class, 1L); // 실제 객체
Member member2 = em.getReference(Member.class, 2L); // 프록시 객체
System.out.println(member1 instanceof Member); // true
System.out.println(member2 instanceof Member); // true. 프록시 객체도 실제객체를 상속받았으므로
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
Member m1 = em.find(Member.class, member1.geId());
Member m2 = em.getReference(Member.class, member1.getId());
- JPA는 동일한 트랜잭션안에서 동일한 PK에 대해, 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.find()를 하거나 em.getReference()를 하더라도 영속성 컨텍스트에 이미 있는 객체를 반환한다.
- 처음에 em.getReference()를 사용하면 프록시 객체를 반환하고, em.find()를 사용해도 프록시 객체를 반환한다.
이때 프록시 객체가 영속성컨텍스트(1차캐시)에 저장된다. 이후 em.find()를 사용해도 이미 1차캐시에 프록시가 존재하므로 프록시가 반환된다.- 반대의 경우 동일한 트랜잭션안에서 처음에 em.find()를 사용하면 실제 엔티티가 반환되고, em.getReference()를 사용해도 실제 엔티티를 반환한다.
이때 실제 엔티티가 영속성컨텍스트(1차캐시)에 저장된다. 이후 em.getReference()를 사용해도 이미 1차캐시에 실제 엔티티가 존재하므로 실제 엔티티가 반환된다.
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
em.flush();
em.clear();
Member refMember = em.getReference(Member.class, member1.getId());
em.clear();
refMember.getUserName();
준영속상태일때, refMember.getUserName(); 를 호출해서 프록시를 초기화하면 예외가 발생한다.
- em.datach(reMember); 특정 엔티티만 준영속 상태로 전환
- em.clear(); 영속성컨텍스트 초기화
- em.close(); 영속성컨텍스트 종료
- 클래스 = 엔티티(@Entity가 붙은 클래스)
- 객체(클래스로 만든 인스턴스) = 엔티티객체(엔티티로 만든 인스턴스) = 실제객체
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setName("hello1");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember = " + findMember.getTeam().getClass());
public class Member {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private Team team;
... getter and setter
}
select member0_.MEMBER_ID as member_i1_1_0_, member0_.createdBy as createdb2_1_0_, member0_.createdDate as createdd3_1_0_, member0_.lastModifiedBy as lastmodi4_1_0_, member0_.lastModifiedDate as lastmodi5_1_0_, member0_.USERNAME as username6_1_0_, member0_.team_TEAM_ID as team_tea7_1_0_ from Member member0_ where member0_.MEMBER_ID=? findMember = class hellojpa.Team$HibernateProxy$zYNtOSkB
- Member의 데이터를 조회해서 Member객체를 생성한다. Team의 데이터는 조회되지않는다. 연관관계의 데이터는 조회하지않는다.
- Team team 필드에는 프록시 객체가 들어가있다. 즉 team 필드는 프록시 객체를 참조한다.
- 이후에 findMember.getTeam().getName();을 호출하면, 프록시가 초기화된다. 지연 로딩으로 Team 프록시를 초기화할 때, JPA는 이미 Member 엔티티 내부에 조회되어 있는 team_id를 사용해서 Team을 SELECT 한다.
- 참고로, 지연로딩일때,
- findMember.getTeam(); // 추가 쿼리 호출 x
- findMember.getTeam().getName(); // 추가 쿼리 호출o
- System.out.println(findMember.getTeam()); // 추가 쿼리 호출o
- System.out.println(findMember.getTeam().getName()); // 추가 쿼리 호출o
- for (Member m : team.getMembers()) // 추가 쿼리 호출o
=> 반복문이 실행되려고 할때 team.getMembers() 컬렉션을 초기화 하기 위해 추가쿼리를 무조건 호출한다.
그리고나서 결과가 있으면 쿼리결과를 가져와서 for문이 실행된다. 결과가 없으면 빈 리스트가 반환되므로 for문은 실행되지않는다.
즉, 리스트에 데이터가 있으면 for문이 실행되지만 데이터가 없어서 빈 리스트이면 for문이 아예 실행되지않는다.- order.getOrderItems().stream() // 추가 쿼리 호출 o
public class Member {
...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn
private Team team;
... getter and setter
}
select member0_.MEMBER_ID as member_i1_1_0_, member0_.createdBy as createdb2_1_0_, member0_.createdDate as createdd3_1_0_, member0_.lastModifiedBy as lastmodi4_1_0_, member0_.lastModifiedDate as lastmodi5_1_0_, member0_.USERNAME as username6_1_0_, member0_.team_TEAM_ID as team_tea7_1_0_, team1_.TEAM_ID as team_id1_4_1_, team1_.createdBy as createdb2_4_1_, team1_.createdDate as createdd3_4_1_, team1_.lastModifiedBy as lastmodi4_4_1_, team1_.lastModifiedDate as lastmodi5_4_1_, team1_.name as name6_4_1_ from Member member0_ left outer join Team team1_ on member0_.team_TEAM_ID=team1_.TEAM_ID where member0_.MEMBER_ID=? findMember = class hellojpa.Team
- Member와 Team의 데이터를 한번에 조회해서 Member객체를 생성한다. 연관관계의 데이터도 함께 조회한다.
- Team team 필드에는 실제 객체가 들어가있다. 즉 team 필드는 실제 객체를 참조한다.