[자바 ORM 표준 JPA 프로그래밍 - 기본편] 08강. 프록시와 연관관계 관리
08강. 프록시와 연관관계 관리
01. 프록시
프록시 기초
em.find()
vsem.getReference()
em.find()
: 데이터베이스를 통해서 실제 엔티티 객체 조회em.getReference()
: 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회- DB에 쿼리가 나가지 않는데 객체가 조회됨
-
target이 진짜 reference를 가리킴
- 예제
-
getReference
Member member = new Member(); member.setUsername("hello"); em.persist(member); em.flush(); em.clear(); Member findMember = em.getReference(Member.class, member.getId()); tx.commit();
Hibernate: /* insert hellojpa.Member */ insert into Member (createdBy, createdDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID) values (?, ?, ?, ?, ?, ?)
-
getReference + ?
Member member = new Member(); member.setUsername("hello"); em.persist(member); em.flush(); em.clear(); Member findMember = em.getReference(Member.class, member.getId()); System.out.println("findMember.id = " + findMember.getId()); // 파라미터로 넣은 ID는 DB에 가지 않아도 알 수 있음 System.out.println("findMember.username = " + findMember.getUsername()); // Username은 DB에 있음. Reference에 없어서 DB에 쿼리를 날림 tx.commit();
Hibernate: /* insert hellojpa.Member */ insert into Member (createdBy, createdDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID) values (?, ?, ?, ?, ?, ?) findMember.id = 1 // DB에서 가져오지 않아도 됨 Hibernate: // DB 찍고 가져옴 select member0_.MEMBER_ID as MEMBER_I1_3_0_, member0_.createdBy as createdB2_3_0_, member0_.createdDate as createdD3_3_0_, member0_.lastModifiedBy as lastModi4_3_0_, member0_.lastModifiedDate as lastModi5_3_0_, member0_.TEAM_ID as TEAM_ID7_3_0_, member0_.USERNAME as USERNAME6_3_0_, team1_.TEAM_ID as TEAM_ID1_7_1_, team1_.createdBy as createdB2_7_1_, team1_.createdDate as createdD3_7_1_, team1_.lastModifiedBy as lastModi4_7_1_, team1_.lastModifiedDate as lastModi5_7_1_, team1_.name as name6_7_1_ from Member member0_ left outer join Team team1_ on member0_.TEAM_ID=team1_.TEAM_ID where member0_.MEMBER_ID=? findMember.username = hello
-
findMember는 Proxy
Member member = new Member(); member.setUsername("hello"); em.persist(member); em.flush(); em.clear(); Member findMember = em.getReference(Member.class, member.getId()); System.out.println("findMember = " + findMember.getClass());
Hibernate: /* insert hellojpa.Member */ insert into Member (createdBy, createdDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID) values (?, ?, ?, ?, ?, ?) findMember = class hellojpa.Member$HibernateProxy$LDM2UMzq
- findMember는 Member가 아니고
Member$HibernateProxy
: Hibernate가 만든 가짜 Proxy
- findMember는 Member가 아니고
-
프록시 객체의 초기화
Member member = em.getReference(Member.class, “id1”);
member.getName();
ex.
member는 프록시 객체를 가져온 상태
- member.getName()을 호출
- 처음에 Proxy를 봤는데, target에 값이 없음
- JPA가 영속성 컨텍스트에 요청 : member 객체 가져와 : 초기화 요청 !!
- 영속성 컨텍스트가 DB 조회해서 실제 Entity 객체를 생성해서 Member를 돌려주고 target에 연결
- 이제 getName() 하면 target의 getName()을 통해 Member의 getName()을 반환
프록시의 특징
- 실제 클래스를 상속 받아서 만들어짐 -> 실제 클래스와 겉모양이 같음
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)
- 프록시 객체는 실제 객체의 참조(target)를 보관
-
프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
- ex. getName()을 호출하면 target에 있는 getName()을 호출
- 처음에는 target 비어 있음 : DB에서 조회한 적이 없으니까
- 프록시 객체는 처음 사용할 때 한 번만 초기화
- 한 번 초기화하면 계속 사용
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님!!!
- 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능한 것
-
예제 : before findMember와 after findMember가 동일
Member member = new Member(); member.setUsername("hello"); em.persist(member); em.flush(); em.clear(); Member findMember = em.getReference(Member.class, member.getId()); System.out.println("before findMember = " + findMember.getClass()); System.out.println("findMember.username = " + findMember.getUsername()); System.out.println("after findMember = " + findMember.getClass()); tx.commit();
Hibernate: /* insert hellojpa.Member */ insert into Member (createdBy, createdDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID) values (?, ?, ?, ?, ?, ?) before findMember = class hellojpa.Member$HibernateProxy$L2EuOtQa Hibernate: select member0_.MEMBER_ID as MEMBER_I1_3_0_, member0_.createdBy as createdB2_3_0_, member0_.createdDate as createdD3_3_0_, member0_.lastModifiedBy as lastModi4_3_0_, member0_.lastModifiedDate as lastModi5_3_0_, member0_.TEAM_ID as TEAM_ID7_3_0_, member0_.USERNAME as USERNAME6_3_0_, team1_.TEAM_ID as TEAM_ID1_7_1_, team1_.createdBy as createdB2_7_1_, team1_.createdDate as createdD3_7_1_, team1_.lastModifiedBy as lastModi4_7_1_, team1_.lastModifiedDate as lastModi5_7_1_, team1_.name as name6_7_1_ from Member member0_ left outer join Team team1_ on member0_.TEAM_ID=team1_.TEAM_ID where member0_.MEMBER_ID=? findMember.username = hello after findMember = class hellojpa.Member$HibernateProxy$L2EuOtQa
- 프록시 객체는 원본 엔티티를 상속 받음. 따라서 타입 체크시 == 가 아닌 instanceof 로 비교
- 예제
-
find 결과를 비교
Member member1 = new Member(); member1.setUsername("member1"); em.persist(member1); Member member2 = new Member(); member2.setUsername("member2"); em.persist(member2); em.flush(); em.clear(); Member m1 = em.find(Member.class, member1.getId()); Member m2 = em.find(Member.class, member2.getId()); System.out.println("m1 == m2 : " + (m1.getClass() == m2.getClass())); // true tx.commit();
-
find 와 getReference 결과를 비교
-
실제로 proxy가 넘어올지 실제 객체가 넘어올지 모르기 때문에 절대 ==으로 type 비교를 하면 안됨
Member member1 = new Member(); member1.setUsername("member1"); em.persist(member1); Member member2 = new Member(); member2.setUsername("member2"); em.persist(member2); em.flush(); em.clear(); 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 (type 비교여서) tx.commit();
-
instanceof로 비교
Member member1 = new Member(); member1.setUsername("member1"); em.persist(member1); Member member2 = new Member(); member2.setUsername("member2"); em.persist(member2); em.flush(); em.clear(); Member m1 = em.find(Member.class, member1.getId()); Member m2 = em.getReference(Member.class, member2.getId()); logic(m1, m2); // m1 == m2 : true // m1 == m2 : true
-
-
- 예제
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
-
예제
Member member1 = new Member(); member1.setUsername("member1"); em.persist(member1); em.flush(); em.clear(); Member m1 = em.find(Member.class, member1.getId()); System.out.println("m1 = " + m1.getClass()); // m1 = class hellojpa.Member // 현재 m1이 영속성 컨텍스트에 올라가 있는 상태 Member reference = em.getReference(Member.class, member1.getId()); System.out.println("reference = " + reference.getClass()); // reference = class hellojpa.Member tx.commit();
- 이유
- JPA는 한 영속성 컨텍스트에서 가져온 것을 비교하면 항상 true를 반환을 보장
- 이미 영속성 컨텍스트에 올라와있는데 Proxy를 가져오는 것이 이점이 없음
- 이유
-
-
한 번 Proxy를 조회하면 em.find에서도 Proxy를 반환
Member member1 = new Member(); member1.setUsername("member1"); em.persist(member1); em.flush(); em.clear(); Member refMember = em.getReference(Member.class, member1.getId()); System.out.println("refMember = " + refMember.getClass()); // refMember = class hellojpa.Member$HibernateProxy$WmuYKyAh Member findMember = em.find(Member.class, member1.getId()); System.out.println("findMember = " + findMember.getClass()); // findMember = class hellojpa.Member$HibernateProxy$WmuYKyAh // findMember 했는데 Proxy를 반환 // 왜냐하면,, 한 번 Proxy를 조회하면 em.find에서 Proxy를 반환 System.out.println("refMember == findMember : " + (refMember == findMember)); // refMember == findMember : true tx.commit();
-
⭐️ 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생 (Hibernate는
org.hibernate.LazyInitializationException
예외)try { Member member1 = new Member(); member1.setUsername("member1"); em.persist(member1); em.flush(); em.clear(); Member refMember = em.getReference(Member.class, member1.getId()); System.out.println("refMember = " + refMember.getClass()); // Proxy em.detach(refMember); // 영속성 컨텍스트에서 꺼내버림 // em.close(); // 이거도 마찬가지 // em.clear(); refMember.getUsername(); // 영속성 컨텍스트의 도움을 받아서 초기화해야하는데 영속성 컨텍스트에서 관리하지 않고 있음 // 따라서 could not initialize proxy tx.commit(); } catch (Exception e) { tx.rollback(); e.printStackTrace(); } finally { em.close(); }
프록시 확인
- 프록시 인스턴스의 초기화 여부 확인 :
emf.getPersistenceUnitUtil().isLoaded(Object entity)
-
초기화 전
Member member1 = new Member(); member1.setUsername("member1"); em.persist(member1); em.flush(); em.clear(); Member refMember = em.getReference(Member.class, member1.getId()); System.out.println("refMember = " + refMember.getClass()); // Proxy System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember)); // false tx.commit();
-
초기화 후
Member member1 = new Member(); member1.setUsername("member1"); em.persist(member1); em.flush(); em.clear(); Member refMember = em.getReference(Member.class, member1.getId()); System.out.println("refMember = " + refMember.getClass()); // Proxy refMember.getUsername(); // 초기화 System.out.println("isLoaded = " + emf.getPersistenceUnitUtil().isLoaded(refMember)); // true tx.commit();
-
-
프록시 클래스 확인 방법 :
entity.getClass().getName()
출력Member member1 = new Member(); member1.setUsername("member1"); em.persist(member1); em.flush(); em.clear(); Member refMember = em.getReference(Member.class, member1.getId()); System.out.println("refMember = " + refMember.getClass()); // refMember = class hellojpa.Member$HibernateProxy$bPWWO9Gu tx.commit();
-
프록시 강제 초기화 :
org.hibernate.Hibernate.initialize(entity);
Member member1 = new Member(); member1.setUsername("member1"); em.persist(member1); em.flush(); em.clear(); Member refMember = em.getReference(Member.class, member1.getId()); System.out.println("refMember = " + refMember.getClass()); Hibernate.initialize(refMember); tx.commit();
Hibernate: call next value for hibernate_sequence Hibernate: /* insert hellojpa.Member */ insert into Member (createdBy, createdDate, lastModifiedBy, lastModifiedDate, USERNAME, MEMBER_ID) values (?, ?, ?, ?, ?, ?) refMember = class hellojpa.Member$HibernateProxy$X96OXpP5 Hibernate: select member0_.MEMBER_ID as MEMBER_I1_3_0_, member0_.createdBy as createdB2_3_0_, member0_.createdDate as createdD3_3_0_, member0_.lastModifiedBy as lastModi4_3_0_, member0_.lastModifiedDate as lastModi5_3_0_, member0_.TEAM_ID as TEAM_ID7_3_0_, member0_.USERNAME as USERNAME6_3_0_, team1_.TEAM_ID as TEAM_ID1_7_1_, team1_.createdBy as createdB2_7_1_, team1_.createdDate as createdD3_7_1_, team1_.lastModifiedBy as lastModi4_7_1_, team1_.lastModifiedDate as lastModi5_7_1_, team1_.name as name6_7_1_ from Member member0_ left outer join Team team1_ on member0_.TEAM_ID=team1_.TEAM_ID where member0_.MEMBER_ID=?
- 참고: JPA 표준은 강제 초기화 없음
- 강제 호출 :
member.getName()
- 강제 호출 :
02. 즉시 로딩과 지연 로딩
지연 로딩 LAZY을 사용해서 프록시로 조회
지연 로딩
- 지연 로딩 : 프록시에서만 조회
- 사용 방법 :
fetch = FetchType.*LAZY*
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.LAZY) //**
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}
JpaMain.java
// 생략
Member m = em.find(Member.class, member1.getId());
System.out.println("m = " + m.getTeam().getClass()); // m = class hellojpa.Team$HibernateProxy$vrnrtZ7q (Proxy)
System.out.println("===============");
m.getTeam().getName(); // 여기서 쿼리가 나감!!
tx.commit();
Hibernate:
call next value for hibernate_sequence
Hibernate:
call next value for hibernate_sequence
Hibernate:
/* insert hellojpa.Team
*/ insert
into
Team
(createdBy, createdDate, lastModifiedBy, lastModifiedDate, name, TEAM_ID)
values
(?, ?, ?, ?, ?, ?)
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(createdBy, createdDate, lastModifiedBy, lastModifiedDate, TEAM_ID, USERNAME, MEMBER_ID)
values
(?, ?, ?, ?, ?, ?, ?)
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_3_0_,
member0_.createdBy as createdB2_3_0_,
member0_.createdDate as createdD3_3_0_,
member0_.lastModifiedBy as lastModi4_3_0_,
member0_.lastModifiedDate as lastModi5_3_0_,
member0_.TEAM_ID as TEAM_ID7_3_0_,
member0_.USERNAME as USERNAME6_3_0_
from
Member member0_
where
member0_.MEMBER_ID=?
m = class hellojpa.Team$HibernateProxy$vrnrtZ7q
===============
Hibernate:
select
team0_.TEAM_ID as TEAM_ID1_7_0_,
team0_.createdBy as createdB2_7_0_,
team0_.createdDate as createdD3_7_0_,
team0_.lastModifiedBy as lastModi4_7_0_,
team0_.lastModifiedDate as lastModi5_7_0_,
team0_.name as name6_7_0_
from
Team team0_
where
team0_.TEAM_ID=?
지연 로딩 LAZY을 사용해서 프록시로 조회
Member member = em.find(Member.class, 1L);
Team team = member.getTeam();
team.getName(); // 실제 team을 사용하는 시점에 초기화(DB 조회)
즉시 로딩 EAGER를 사용해서 함께 조회
: Member와 Team을 자주 함께 사용하는 경우
즉시 로딩
- member1를 로딩할 때 team1까지 join해서 한 번에 가져옴
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne(fetch = FetchType.EAGER) //**
@JoinColumn(name = "TEAM_ID")
private Team team;
..
}
JpaMain.java
// 생략
Member m = em.find(Member.class, member1.getId());
System.out.println("m = " + m.getTeam().getClass()); // m = class hellojpa.Team
tx.commit();
실행 결과
- 즉시 로딩이기 때문에 Proxy가 필요하지 않고 한 번에 다 땡겨옴
Hibernate:
/* insert hellojpa.Team
*/ insert
into
Team
(createdBy, createdDate, lastModifiedBy, lastModifiedDate, name, TEAM_ID)
values
(?, ?, ?, ?, ?, ?)
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(createdBy, createdDate, lastModifiedBy, lastModifiedDate, TEAM_ID, USERNAME, MEMBER_ID)
values
(?, ?, ?, ?, ?, ?, ?)
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_3_0_,
member0_.createdBy as createdB2_3_0_,
member0_.createdDate as createdD3_3_0_,
member0_.lastModifiedBy as lastModi4_3_0_,
member0_.lastModifiedDate as lastModi5_3_0_,
member0_.TEAM_ID as TEAM_ID7_3_0_,
member0_.USERNAME as USERNAME6_3_0_,
team1_.TEAM_ID as TEAM_ID1_7_1_,
team1_.createdBy as createdB2_7_1_,
team1_.createdDate as createdD3_7_1_,
team1_.lastModifiedBy as lastModi4_7_1_,
team1_.lastModifiedDate as lastModi5_7_1_,
team1_.name as name6_7_1_
from
Member member0_
left outer join
Team team1_
on member0_.TEAM_ID=team1_.TEAM_ID
where
member0_.MEMBER_ID=?
m = class hellojpa.Team
==============
프록시와 즉시로딩 주의
- 가급적 지연 로딩만 사용(특히 실무에서)
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킴
- N+1 문제 : 최초로 나간 쿼리(1) 때문에 추가 쿼리가 발생(N)
ex.
Team teamA = new Team(); teamA.setName("teamA"); em.persist(teamA); Team teamB = new Team(); teamB.setName("teamB"); em.persist(teamB); Member member1 = new Member(); member1.setUsername("member1"); member1.setTeam(teamA); em.persist(member1); Member member2 = new Member(); member2.setUsername("member2"); member2.setTeam(teamB); em.persist(member2); em.flush(); em.clear(); List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList(); tx.commit();
Hibernate: /* select m from Member m */ select member0_.MEMBER_ID as MEMBER_I1_3_, member0_.createdBy as createdB2_3_, member0_.createdDate as createdD3_3_, member0_.lastModifiedBy as lastModi4_3_, member0_.lastModifiedDate as lastModi5_3_, member0_.TEAM_ID as TEAM_ID7_3_, member0_.USERNAME as USERNAME6_3_ from Member member0_ Hibernate: select team0_.TEAM_ID as TEAM_ID1_7_0_, team0_.createdBy as createdB2_7_0_, team0_.createdDate as createdD3_7_0_, team0_.lastModifiedBy as lastModi4_7_0_, team0_.lastModifiedDate as lastModi5_7_0_, team0_.name as name6_7_0_ from Team team0_ where team0_.TEAM_ID=? Hibernate: select team0_.TEAM_ID as TEAM_ID1_7_0_, team0_.createdBy as createdB2_7_0_, team0_.createdDate as createdD3_7_0_, team0_.lastModifiedBy as lastModi4_7_0_, team0_.lastModifiedDate as lastModi5_7_0_, team0_.name as name6_7_0_ from Team team0_ where team0_.TEAM_ID=?
- Member를 조회하였는데 즉시 로딩 때문에 Team도 바로 조회
-
지연로딩하면 프록시에서만 가져올 것을.. (Team은 프록시로 박혀있음)
// @ManyToOne(fetch = FetchType.LAZY) Hibernate: /* select m from Member m */ select member0_.MEMBER_ID as MEMBER_I1_3_, member0_.createdBy as createdB2_3_, member0_.createdDate as createdD3_3_, member0_.lastModifiedBy as lastModi4_3_, member0_.lastModifiedDate as lastModi5_3_, member0_.TEAM_ID as TEAM_ID7_3_, member0_.USERNAME as USERNAME6_3_ from Member member0_
- 지연로딩 해결 방법
- 일단 모든 연관 관계를 지연로딩으로 깔고
-
방법 1. JPQL fetch join : 동적으로 원하는 Entity만 선택해서 가져옴
Team teamA = new Team(); teamA.setName("teamA"); em.persist(teamA); Team teamB = new Team(); teamB.setName("teamB"); em.persist(teamB); Member member1 = new Member(); member1.setUsername("member1"); member1.setTeam(teamA); em.persist(member1); Member member2 = new Member(); member2.setUsername("member2"); member2.setTeam(teamB); em.persist(member2); em.flush(); em.clear(); List<Member> members = em.createQuery("select m from Member m join fetch m.team", Member.class).getResultList(); tx.commit();
// 지연로딩이지만 Member와 Team을 join해서 한 번에 같이 가져옴 Hibernate: /* select m from Member m join fetch m.team */ select member0_.MEMBER_ID as MEMBER_I1_3_0_, team1_.TEAM_ID as TEAM_ID1_7_1_, member0_.createdBy as createdB2_3_0_, member0_.createdDate as createdD3_3_0_, member0_.lastModifiedBy as lastModi4_3_0_, member0_.lastModifiedDate as lastModi5_3_0_, member0_.TEAM_ID as TEAM_ID7_3_0_, member0_.USERNAME as USERNAME6_3_0_, team1_.createdBy as createdB2_7_1_, team1_.createdDate as createdD3_7_1_, team1_.lastModifiedBy as lastModi4_7_1_, team1_.lastModifiedDate as lastModi5_7_1_, team1_.name as name6_7_1_ from Member member0_ inner join Team team1_ on member0_.TEAM_ID=team1_.TEAM_ID
- 방법 2.
@EntityGraph
annotation - 방법 3. Batch Size : 쿼리가 N+1이 아니라 1+1로 나감
@ManyToOne
,@OneToOne
은 기본이 즉시 로딩 : LAZY로 설정@OneToMany
,@ManyToMany
는 기본이 지연 로딩
03. 영속성 전이 : CASCADE
영속성 전이: CASCADE
- (연관관계, 즉시 로딩, 지연 로딩과 관계 없음)
-
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때
ex. 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장
예제.
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent")
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
// getter, setter (생략)
}
@Entity
public class Child {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
// getter, setter (생략)
}
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
// persist를 3번 해줘야 함. 그런데 좀 귀찮은데 ..
em.persist(parent);
em.persist(child1);
em.persist(child2);
em.flush();
em.clear();
tx.commit();
Hibernate:
/* insert hellojpa.Parent
*/ insert
into
Parent
(name, id)
values
(?, ?)
Hibernate:
/* insert hellojpa.Child
*/ insert
into
Child
(name, parent_id, id)
values
(?, ?, ?)
Hibernate:
/* insert hellojpa.Child
*/ insert
into
Child
(name, parent_id, id)
values
(?, ?, ?)
이때, parent를 persist할 때 child도 persist 되게 하려면 : CASCADING
-
사용방법
// Parent.java @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) 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.flush();
em.clear();
tx.commit();
// parent만 persist 했는데, child1, child2도 persist됨
Hibernate:
/* insert hellojpa.Parent
*/ insert
into
Parent
(name, id)
values
(?, ?)
Hibernate:
/* insert hellojpa.Child
*/ insert
into
Child
(name, parent_id, id)
values
(?, ?, ?)
Hibernate:
/* insert hellojpa.Child
*/ insert
into
Child
(name, parent_id, id)
values
(?, ?, ?)
🚨 주의
- 영속성 전이는 연관관계를 매핑하는 것과 아무 관련 X
- 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐
- 단일 소유자일 때만(정말 부모가 하나일 때) 사용 ex. parent만 child를 관리. 게시판의 첨부파일 경로 등
CASCADE의 종류
| 종류 | 상세 | | — | — | | ALL | 모두 적용 | | PERSIST | 영속 | | REMOVE | 삭제 | | MERGE | 병합 | | REFREPSH | Refresh | | DETACH | Detach |
- 보통 이 중 ALL, PERSIST 사용
04. 고아 객체
고아 객체
- 고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
orphanRemoval = true
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0); //자식 엔티티를 컬렉션에서 제거
// Delete 쿼리가 나감
DELETE FROM CHILD WHERE ID=?
예제
@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<>();
// parent가 지워지면 그 하위 childList에 있는 child도 지워진다
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
// getter, setter (생략)
}
// JpaMain.java
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();
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0); // 부모 제거 -> orphanRemoval이 동작
tx.commit();
Hibernate:
/* insert hellojpa.Parent
*/ insert
into
Parent
(name, id)
values
(?, ?)
Hibernate:
/* insert hellojpa.Child
*/ insert
into
Child
(name, parent_id, id)
values
(?, ?, ?)
Hibernate:
/* insert hellojpa.Child
*/ insert
into
Child
(name, parent_id, id)
values
(?, ?, ?)
Hibernate:
select
parent0_.id as id1_7_0_,
parent0_.name as name2_7_0_
from
Parent parent0_
where
parent0_.id=?
Hibernate:
select
childlist0_.parent_id as parent_i3_2_0_,
childlist0_.id as id1_2_0_,
childlist0_.id as id1_2_1_,
childlist0_.name as name2_2_1_,
childlist0_.parent_id as parent_i3_2_1_
from
Child childlist0_
where
childlist0_.parent_id=?
Hibernate:
/* delete hellojpa.Child */ delete
from
Child
where
id=?
🚨 주의
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
- 참조하는 곳이 하나일 때 사용해야함!
- ⭐️ 특정 엔티티가 개인 소유할 때 사용
@OneToOne
,@OneToMany
만 가능- 참고 : 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은
CascadeType.REMOVE
처럼 동작한다.
CascadeType 지정 안하면 child도 각각 persist 해줘야 ,, (안하면 parent만 delete 됨)
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
childList.add(child);
child.setParent(this);
}
// getter, setter (생략)
}
// JpaMain.java
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());
em.remove(findParent);
tx.commit();
Hibernate:
/* insert hellojpa.Parent
*/ insert
into
Parent
(name, id)
values
(?, ?)
Hibernate:
/* insert hellojpa.Child
*/ insert
into
Child
(name, parent_id, id)
values
(?, ?, ?)
Hibernate:
/* insert hellojpa.Child
*/ insert
into
Child
(name, parent_id, id)
values
(?, ?, ?)
Hibernate:
select
parent0_.id as id1_7_0_,
parent0_.name as name2_7_0_
from
Parent parent0_
where
parent0_.id=?
Hibernate:
select
childlist0_.parent_id as parent_i3_2_0_,
childlist0_.id as id1_2_0_,
childlist0_.id as id1_2_1_,
childlist0_.name as name2_2_1_,
childlist0_.parent_id as parent_i3_2_1_
from
Child childlist0_
where
childlist0_.parent_id=?
Hibernate:
/* delete hellojpa.Child */ delete
from
Child
where
id=?
Hibernate:
/* delete hellojpa.Child */ delete
from
Child
where
id=?
Hibernate:
/* delete hellojpa.Parent */ delete
from
Parent
where
id=?
05. 영속성 전이 + 고아 객체, 생명 주기
영속성 전이 + 고아 객체, 생명주기
CascadeType.ALL
+orphanRemoval=true
- 스스로 생명주기를 관리하는 엔티티는
em.persist()
로 영속화,em.remove()
로 제거 - 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
- 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용
Leave a comment