2. 즉시 로딩과 지연 로딩
- 단순히 member 정보만 사용하는 비즈니스 로직
- member 클래스
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
→ 멤버 클래스만 db에서 조회한다는 뜻
- JpaMain
Member m = em.find(Member.class, member1.getId());
- 결과
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_3_0_,
member0_.INSERT_MEMBER as INSERT_M2_3_0_,
member0_.createdDate as createdD3_3_0_,
member0_.UPDATE_MEMBER as UPDATE_M4_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=?
→ 멤버만 가져온다
- JpaMain
try {
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team);
em.persist(member1);
em.flush();
em.clear();
Member m = em.find(Member.class, member1.getId());
System.out.println("m = " + m.getTeam().getClass());
tx.commit();
}
- 결과
Hibernate:
select
member0_.MEMBER_ID as MEMBER_I1_3_0_,
member0_.INSERT_MEMBER as INSERT_M2_3_0_,
member0_.createdDate as createdD3_3_0_,
member0_.UPDATE_MEMBER as UPDATE_M4_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$bVsFQaGi
Member member = em.find(Member.class, 1L);
Team team = member.getTeam();
team.getName(); // 실제 team을 사용하는 시점에 초기화(DB조회)
Member와 Team을 자주 함께 사용한다면? → 즉시 로딩(EAGER)를 사용해서 함께 조회
@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;
..
}
→ JPA 구현체는 가능하면 조인을 사용해서 SQL을 한번에 함께 조회하려 함!
프록시와 즉시로딩 주의
1. 가급적 지연 로딩만 사용(특히 실무에서)
적은 테이블에서는 sql 구문이 조금 더 나가는 차이겠지만 수십, 수백개의 테이블에서는 성능 차이가 심하게 난다.
2. 즉시 로딩을 적용하면 예상하지 못한 SQL 발생
3. 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
try {
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team);
em.persist(member1);
em.flush();
em.clear();
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
tx.commit();
}
- select 구문이 두 번 나가는 이유
- select m from Member m 이 구문이 SQL로 번역(SQL: select * from Member)
- 구문을 실행할 때 Team 값이 있어야 함 → SQL: select * from Team where TEAM_ID = xxx
Hibernate:
/* select
m
from
Member m */ select
member0_.MEMBER_ID as MEMBER_I1_3_,
member0_.INSERT_MEMBER as INSERT_M2_3_,
member0_.createdDate as createdD3_3_,
member0_.UPDATE_MEMBER as UPDATE_M4_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_.INSERT_MEMBER as INSERT_M2_7_0_,
team0_.createdDate as createdD3_7_0_,
team0_.UPDATE_MEMBER as UPDATE_M4_7_0_,
team0_.lastModifiedDate as lastModi5_7_0_,
team0_.name as name6_7_0_
from
Team team0_
where
team0_.TEAM_ID=?
- N+1인 이유 → 처음 쿼리(select m from Member m)를 1개 날렸는데 그것 때문에 N개의 추가 쿼리가 나가기 때문에 N+1 문제라고 함.
- 해결 방법
- 패치 조인(기본): 동적으로 내가 원하는 애들을 선택해서 한방에 가져오는 것.
- 엔티티 그래프와 어노테이션
- 배치 사이즈(1+1)
4. @ManyToOne, @OneToOne은 기본이 즉시 로딩 → LAZY로 설정
5. @OneToMany, @ManyToMany는 기본이 지연 로딩
3. 지연 로딩 활용
이론적인 것 - 실무에서는 무조건 지연 로딩!!
- Member와 Team은 자주 함께 사용 → 즉시 로딩
- Member와 Order는 가끔 사용 → 지연 로딩
- Order와 Product는 자주 함께 사용 → 즉시 로딩
지연 로딩 활용 -실무
- 모든 연관관계에 지연 로딩을 사용해라!
- 실무에서 즉시 로딩을 사용하지 마라!
- JPQL fetch 조인이나, 엔티티 그래프 기능을 사용!
- 즉시 로딩은 상상하지 못한 쿼리가 나간다!
4. 영속성 전이: CASCADE
정의
특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때
예시
부모 엔티티를 저장할 때 자식 엔티티도 함께 저장
예시) cascade = CascadeType.ALL을 설정하면 em.persist(parent)만 해도 child1, child2까지 함께 persist 된다.
try {
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
tx.commit();
}
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
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
(?, ?, ?)
영속성 전이: 저장
@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)
CASCADE 주의 사항
- 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다.
- 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.
- 소유자가 하나일 때만 사용 가능. ex) parent와 child에서 child를 parent만 사용할 때는 가능하지만 다른 클래스에서도 child를 사용하면 cascade를 사용하면 안된다.
CASCADE 종류(ALL, PERSIST만 대부분 사용)
ALL: 모두 적용(대부분)
PERSIST: 영속(저장할 때만 사용할 때)
REMOVE: 삭제
MERGE: 병합
REFRESH: REFRESH
DETACH: DETACH
5. 고아 객체
기능
고아 객체 제거: 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
orphanRemoval = true
Parent parent1 = em.find(Parent.class, id); parent1.getChildren().remove(0);
//자식 엔티티를 컬렉션에서 제거
DELETE FROM CHILD WHERE ID=?
예시코드
try {
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);
tx.commit();
}
→ orphanRemoval은 관계가 끊어진 자식을 컬렉션에서 제거한다(em.flush()와 em.clear()로 값 넘긴 후 findParent.getChildList().remove(0)을 통해 0번지 값의 연관관계를 끊자 컬렉션에서도 제거됨.
주의사항
1. 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
2. 참조하는 곳이 하나일 때 사용해야 한다.
3. 특정 엔티티가 개인 소유할 때 사용
4. @OneToOne, @OneToMany만 가능
5. 참고: 개념적으로 부모를 제거하면 자식은 고아가 된다.
→ 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.
- 예시코드
try {
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();
}
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
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=?
결과: 자식까지 다 지워진다.
6. 영속성 전이 + 고아 객체, 생명주기
1. CascadeType.ALL + orphanRemoval=true
2. 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
3. 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있음
4. 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용
7. 실전 예제 - 5. 연관관계 관리
- @OneToOne 또는 @OneToMany만 cascade ALL 설정
'TIL' 카테고리의 다른 글
[TIL-11 / 230629] 이분 탐색 알고리즘, 깃허브 (0) | 2023.06.29 |
---|---|
[TIL-10 / 230628] JPA 값 타입(임베디드 타입, 값 타입 컬렉션) (0) | 2023.06.28 |
[TIL-8 / 230623] JPA 프록시 (0) | 2023.06.24 |
[TIL-7 / 230622] JPA 다양한 연관관계 매핑과 고급매핑 (0) | 2023.06.23 |
[TIL-6 / 230621] JPA 다양한 연관관계 매핑 (0) | 2023.06.21 |