// Member findMember = em.find(Member.class, member.getId());
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember.getClass() = " + findMember.getClass());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
findMember.getClass() = class hellojpa.Member$HibernateProxy$ZHmYRRE5
findMember.id = 1
getReference 시점에는 select 쿼리가 나가지 않고 밑에 print 문을 호출 하는 순간 select 쿼리가 나가게 된다. 그냥 프록시 객체를 가져온다.
프록시 특징
프록시 객체는 처음 사용할 때 한번만 초기화
프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
프록시 객체는 원 본 엔티티를 상속 받음, 따라서 타입 체크시 주의 해야함 ( 비교할떄는 == 대신 instance of 사용 ) 언제든지 프록시가 올 수 있다. 프록시는 상속관계임으로 instance of 비교를 하면 true가 뜬다.
영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
그래서 em find == em.getReference 를 하면 항상 true가 나온다.
같은 transaction 안에서 2개의 호출을 했을 경우 만 -> repeatable read를 보장하기 떄문에
한 tx내에서 em.getReference 를 한번이라도 호출 했으면 em.find에서도 proxy를 return 하게 된다 -> repeatable read를 보장하기 위해서
영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생 (하이버네이트는 org.hibernate.LazinitializationException 예외를 터트림) , detach(), clear() , close() 도 마찬가지 exception 을 불러온다.
em.getReference 로 받아온 refMember를 detach 한후에 refMember.getUsername()을 할시에 위와 같은 Exception을 발생시킨다. -> 영속성 context에서 더 이상 관리하지 않기 때문이다.
하지만 Hibernate 5.4.1 Final 버전에서는 단순히 Session (EntityManager 가 종료) 이 종료되었다고 exception 을 띄우는게 아니라 transaction 도 종료가 되어있는지 확인을 하게 된다.
프록시 객체의 초기화
- 처음에 getName()을 통해서 Member target이 null 임을 확인한다.
- 영속성 컨텍스트에게 초기화를 요청한다.
- 영속성 컨텍스트가 DB에 쿼리를 날려 Member 엔티티를 조회해온다.
- target 이 Meber를 가르키게되고 메소드를 호출해준다.
프록시 확인
프록시 인스턴스의 초기화 여부 확인 = PersistenceUnitUtil.isLoaded(Object entity)
entityManagerFactory 로붙어 getPersistenceUnitUtil 을 통해 얻을 수 있다.
프록시 클래스 확인 방법 = entity.getClass().getName()
프록시 강제 초기화 = org.hibernate.Hibernate.initialize(entity), 강제호출도 마찬가지 (member.getName())
즉시 로딩과 지연 로딩
Hibernate:
select
member0_.MEMBER_ID as member_i1_4_0_,
member0_.createdBy as createdb2_4_0_,
member0_.createdDate as createdd3_4_0_,
member0_.lastModifiedBy as lastmodi4_4_0_,
member0_.lastModifiedDate as lastmodi5_4_0_,
member0_.LOCKER_ID as locker_i7_4_0_,
member0_.TEAM_ID as team_id8_4_0_,
member0_.USERNAME as username6_4_0_,
locker1_.id as id1_3_1_,
locker1_.name as name2_3_1_,
team2_.TEAM_ID as team_id1_8_2_,
team2_.createdBy as createdb2_8_2_,
team2_.createdDate as createdd3_8_2_,
team2_.lastModifiedBy as lastmodi4_8_2_,
team2_.lastModifiedDate as lastmodi5_8_2_,
team2_.name as name6_8_2_
from
Member member0_
left outer join
Locker locker1_
on member0_.LOCKER_ID=locker1_.id
left outer join
Team team2_
on member0_.TEAM_ID=team2_.TEAM_ID
where
member0_.MEMBER_ID=?
지연로딩(Lazy)를 사용하지 않은 상태에서는 단순히 Member m = em.find() 만 해도 연관된 모든 테이블을 조인 하여 조회한다. 하지만
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name ="TEAM_ID")
private Team team;
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);
member1.setTeam(team);
em.flush();
em.clear();
Member m = em.find(Member.class, member1.getId());
System.out.println("m.getTeam().getClass() = " + m.getTeam().getClass());
System.out.println("========================");
m.getTeam().getName();
System.out.println("========================");
tx.commit();
Hibernate:
select
member0_.MEMBER_ID as member_i1_4_0_,
member0_.createdBy as createdb2_4_0_,
member0_.createdDate as createdd3_4_0_,
member0_.lastModifiedBy as lastmodi4_4_0_,
member0_.lastModifiedDate as lastmodi5_4_0_,
member0_.LOCKER_ID as locker_i7_4_0_,
member0_.TEAM_ID as team_id8_4_0_,
member0_.USERNAME as username6_4_0_
from
Member member0_
where
member0_.MEMBER_ID=?
m.getTeam().getClass() = class hellojpa.Team$HibernateProxy$lIwU9Cxb
LAZY 옵션을 적용해주면 이때는 테이블을 다 조인해서 불러오지않고 딱 Member만 select해오는 것을 볼수 있다.
또한 아직 사용하지않은 Team 객체는 프록시를 return 해주는 것도 확인가능하다. m.getTeam().getClass()를 하면 위와같이 프록시 객체를 가지고 있는것을 확인할 수 있다.
========================
Hibernate:
select
team0_.TEAM_ID as team_id1_8_0_,
team0_.createdBy as createdb2_8_0_,
team0_.createdDate as createdd3_8_0_,
team0_.lastModifiedBy as lastmodi4_8_0_,
team0_.lastModifiedDate as lastmodi5_8_0_,
team0_.name as name6_8_0_
from
Team team0_
where
team0_.TEAM_ID=?
========================
m.getTeam().getName() 팀 객체를 사용하는 순간 이때 team table을 호출하여서 필요한 값을 찾는다.
이와는 반대로 즉시로딩 Eager가 존재한다.
member를 쓸 때 비즈니스 로직에서 90%는 팀을 같이 쓴다면 굳이 지연로딩을 통해 쿼리를 2번 날리는 것보다 즉시로딩을 통해 join해서 한방 쿼리로 가져오는것이 유리하다.
프록시와 즉시로딩 주의
가급적 지연 로딩만 사용(특히 실무에서)
즉시 로딩을 적용하면 예상하지못한 Sql 이 발생한다.
즉시 로딩은 JPQL에서 N+1 문제를 일으킨다. 최초 쿼리가 1이고 그로인하여 발생하는 쿼리가 추가로 N개가 나간단 뜻이다.
@ManytoOne, @OneToOne은 기본이 즉시로딩임으로 LAZY 로 설정을 해주어야한다.
@OneToMany , @ManyToMany는 기본이 지연로딩
영속성 전이: CASCADE
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때
예) 부모 엔티티를 저장할 때 자식 엔티티 도 함께 저장
@Entity
public class Parent
{
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child)
{
childList.add(child);
child.setParent(this);
}
@Entity
public class Child
{
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
// em.persist(child1);
// em.persist(child2);
parent 만 영속성 켄텍스안에 넣어주었는데 저절로 child1, 2 도 자동으로 persist되었다.
영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없음
엔티티를 영속화 할때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.
언제써야할것인가? 연관관계가 위에처럼 간단할때 써야한다 즉 소유자가 하나일때 써야한다. 다른곳에서도 Child에 연관관계가 있다하면 쓰지 않는것이 좋다.
고아객체
고아 객체 제거 = 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
orphanRemoval=true
@Entity
public class Parent
{
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL,orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
// em.persist(child1);
// em.persist(child2);
em.flush();
em.clear();
Parent findparent = em.find(Parent.class, parent.getId());
findparent.getChildList().remove(0);
tx.commit();
Hibernate:
/* delete hellojpa.Child */ delete
from
Child
where
id=?
childlist 에서 삭제 된 녀석은 Delete 쿼리가 날라가면서 삭제가 된다.
고아객체 주의사항
Cascade와 마찬가지로 참조하는 곳이 하나일 때 사용해야한다. 특정 엔티티가 개인 소유할때만 사용.
@OneToOne, @OneToMany 만 가능하다.
부모객체를 제거 하면 당연히 그안에있는 childlist들도 delete query를 통해서 다 날라간다.
CascadeType.ALL + orphanRemovel = true 이 두개의 옵션을 켜주면
부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있음
도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할때 유용하다.
'WEB > JPA' 카테고리의 다른 글
김영한 (ORM 표준 JPA 프로그래밍 10) 객체지향 쿼리 언어 소개 (0) | 2021.03.18 |
---|---|
김영한 (ORM 표준 JPA 프로그래밍 9) 값 타입 (0) | 2021.03.16 |
김영한 (ORM 표준 JPA 프로그래밍 7) 고급 매핑 (0) | 2021.03.13 |
김영한 (ORM 표준 JPA 프로그래밍 6) 다양한 연관관계 매핑 (0) | 2021.03.08 |
김영한 (ORM 표준 JPA 프로그래밍 5) 연관관계 매핑 기초 (0) | 2021.03.07 |