em.find한번으로 member와 team객체를 동시에 가져오고 싶은경우 아래처럼 사용할 수 있습니다.
Member findMember = em.find(Member.class, member.getId());
Team findMemberTeam = findMember.getTeam();
System.out.println("회원 이름: " + findMember.getUsername());
System.out.println("소속팀: " + findMemberTeam.getName());
당연히 아래처럼 연관관계 매핑이 되어있어야하죠.
...for..Member
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
...
...for...Team
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
...
그런데, 만약 Member만 쓰고싶어하는경우에도 em.find(Member.class, member.getId()); 를 사용 하면 아래처럼 쿼리가 실행됩니다.
즉, 소 잡을 칼로 닭을 잡아버리는 그런 상황입니다.
이를 해결하기위해 JPA에서 지원해주는것이 프록시입니다.
...
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("회원 아이디: " + findMember.getId());
System.out.println("회원 이름: " + findMember.getUsername());
...
em.getReference(Member.class, member.getId());로 회원을 검색하게되면 쿼리는 아래와 같이 실행되게됩니다.
회원 아이디의 경우 DB의 쿼리를 실행시킬 필요없이 알수있기때문에 조회 없이 가져오지만, 회원 이름은 가져올때 데이터가 없기때문에 DB에 쿼리를 실행시켜 가져오게 됩니다.
System.out.println("Member class Info : " + findMember.getClass());
이름이 Member가 아닌걸로 확인되어, 하이버네이트가 강제로 만든 프록시 객체입니다.
실제 클래스를 상속 받아서 만들어지며, 실제 클래스와 겉 모양이 같습니다.
사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됩니다.
프록시 객체는 실제 객체의 참조(target)를 보관하며, 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출합니다.
Member member = em.getReference(Member.class, “id1”); // 프록시 객체 호출
member.getName(); // 그림으로 설명
Member m1 = em.find(Member.class, member1.getId());
Member m2 = em.getReference(Member.class, member2.getId());
System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass())); // false
System.out.println("m1 == Member : " + (m2 instanceof Member)); // true
Member m1 = em.find(Member.class, member1.getId());
System.out.println("m1 class : " + m1.getClass()); // m1 class : class hello.Member
Member reference = em.getReference(Member.class, member1.getId());
System.out.println("reference : " + reference.getClass()); // reference : class hello.Member
em.find(Member.class, member1.getId());로 영속성 컨텍스트에 Member정보를 조회한 경우 em.getReference(Member.class, member1.getId());를 사용하여 프록시를 사용해서 굳이 영속성컨텍스트에 있는 객체를 또 타겟으로 설정하여 만들어서 얻을 이점이 없습니다.
System.out.println("m1 == reference : " + (m1 == reference)); // true
em.getReference 사용하여 가져온 객체라도, 영속성컨텍스트에 존재하는 실제 객체를 가져왔기때문에 위와 같은 상황에서 true 출력되는것을 확인 할 수 있습니다.
Member refMember = em.getReference(Member.class, member1.getId());
System.out.println("refMember : " + refMember.getClass()); // proxy 객체
em.detach(refMember); // 영속성 컨텍스트에서 해제
String username = refMember.getUsername(); // 에러 발생
System.out.println("username : " + username);
refMember를 영속성컨텍스트에서 해제 함으로, 더이상 영속성 컨텍스트의 도움을 받지 못합니다.
더이상 프록시 객체를 초기화 하지 못함을 의미하게됩니다.
Member 객체를 조회할때 무조건 Team객체도 항상 조회해야할까요?
이를해결하기위해 JPA에서는 지연로딩을 지원합니다.
@ManyToOne(fetch = FetchType.LAZY) // 지연로딩 설정
@JoinColumn(name = "TEAM_ID")
private Team team;
...
Member findMember = em.find(Member.class, member1.getId());
...
이처럼, 지연로딩 설정 후 em.find를 통하면 Team정보는 조회하지않는걸 확인 할 수 있습니다.
Member findMember = em.find(Member.class, member1.getId());
System.out.println("==============================");
String name = findMember.getTeam().getName(); // Query 호출
처음 Member를 조회후 Team에 getName()호출 시 Team Select 쿼리가 실행되는걸 확인 할 수 있습니다. (프록시 객체 초기화)
지연로딩으로 객체를 호출 시 에는 프록시 객체를 조회합니다.
Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember : " + findMember.getTeam().getClass());
지연로딩으로 설정 후 find 조회 시 객체의 class가 프록시인걸 확인 할 수 있습니다.
단, Member와 Team을 자주 함께 사용한다면?
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
Member 조회 시 한번에 Team정보까지 조회하며, 프록시를 사용하지않습니다.
프로젝트 마다 설정에 따라 지연로딩을 할지 또는 즉시로딩을 할지 선택하면됩니다.
...
List<Member> memberList = em.createQuery("select m from Member m", Member.class).getResultList();
...
JPQL로 조회 시 아래와 같이 SELECT문이 2번 호출되게 됩니다.
JPQL은 개발자가 직접 적은 SQL로 호출되기 때문에,
일단 select m from Member m
SQL을 실행하게 됩니다.
그 후, Member 엔티티에 설정된 Team이 즉시로딩 설정이기때문에 Team을 조회하는 쿼리가 또 실행되게 되어 위 사진처럼 2번에 호출이 되게됩니다.
테스트 용도로 Team엔티티 1개로만 했지, 만약 점점 많아지게된다면 SQL은 계속적으로 증가되게 됩니다.
이를 해결하기위해 모든 로딩은 지연로딩으로 설정 후 FetchJoin을 사용합니다.
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
...
...
List<Member> memberList = em.createQuery(
"select m from Member m join fetch m.team", Member.class).getResultList();
...
지연로딩이지만, 즉시로딩처럼 SQL에 team정보가 조회된걸 확인 할 수 있습니다.
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들도 싶을 때 사용합니다.
Parent와 Child 엔티티2개, 1:N 양방향으로 연관관계가 설정되있는 예제입니다.
@Entity
@Setter
@Getter
public class Parent {
@Id @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "PARENT_ID")
private Long id;
@Column(name = "NAME")
private String name;
@OneToMany(mappedBy = "parent")
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
this.childList.add(child);
child.setParent(this);
}
}
@Entity
@Getter
@Setter
public class Child {
@Id @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "CHILD_ID")
private Long id;
@Column(name = "NAME")
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);
...
저장로직을 보면, em.persist가 너무 빈번하게 일어납니다.
이때, em.persist(parent); 한번으로
em.persist(child2); em.persist(child1); 로직을 줄일수있는방법이 CASCADE입니다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) // CASCADE 설정!
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);
...
위처럼 child에 2번 INSERT된걸 확인할수있습니다.
연관관계와는 상관이없으며, em.persist(parent); 할때 parent안에 childList 모두를 persist해주는 설정입니다.
고아 객체 제거, 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 설정
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(0); // 연결 해제
// 자식 엔티티를 컬렉션에서 제거
Delete 쿼리가 실행된걸 확인할수 있습니다.
CascadeType.ALL + orphanRemoval=true
스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
em.flush();
em.clear();
// Child의 모든 생명주기는 Parent가 관리
Parent findParent = em.find(Parent.class, parent.getId());
// 실행 시 Child는 모두 삭제 됨
em.remove(findParent);