JPA
프록시
No-ah98
2022. 4. 7. 17:50
목차
프록시
- 프록시에 대해 설명하기 전에, 이 기술이 나오게 된 배경을 알아보자
- Member와 Team이 연관 관계 되어있다고 했을 때, Member를 조회할 때 Team도 함께 조회 해야할까?
- 비즈니스 로직에 따라서는 Member만 조회하는 경우가 있을 것이다. 즉, Team까지 조회 할 필요가 없을 때 다 같이 조회하는거는 낭비다.
- 이를 지연로딩과 프록시라는 개념으로 해결할 수 있다.
프록시 기초
- 지연 로딩을 이해하기 위해서는 프록시 개념에 대한 이해가 필요하다.
- JPA에서는 em.find() 와 em.getReference() 메소드를 제공한다.
- em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
- DB 쿼리는 안나가는데, 객체가 조회됨
- em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
- em.find() 멤버 조회 -> DB에 쿼리가 바로 발생
Member member = new Member();
member.setCreatedBy("creator");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
tx.commit();
Hibernate:
/* insert hello.jpa.Member
*/ insert
into
Member
(id, createdBy, createdDate, lastModifiedBy, lastModifiedDate, age, description, locker_id, roleType, name)
values
(null, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate:
select
member0_.id as id1_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_.age as age6_4_0_,
member0_.description as descript7_4_0_,
member0_.locker_id as locker_10_4_0_,
member0_.roleType as roleType8_4_0_,
member0_.team_id as team_id11_4_0_,
member0_.name as name9_4_0_,
locker1_.id as id1_3_1_,
locker1_.name as name2_3_1_,
team2_.id as 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_.id
where
member0_.id=?
findMember.id = 1
findMember.username = creator
- em.getReference() 멤버 조회 -> 실제로 필요한 시점에 데이터베이스에 쿼리가 나간다.
- findMember.username 필드 출력할 때, DB에서 조회가 필요하므로 쿼리가 발생
- findMember.getClass()를 통해 객체를 확인해보면 Member객체가 아닌, 하이버 네이트가 강제로 만든 가짜 클래스 HibernateProxy 객체를 확인할 수 있다.
Member member = new Member();
member.setUsername("creator");
em.persist(member);
em.flush();
em.clear();
Member findMember = em.getReference(Member.class, member.getId());
System.out.println("findMember = " + findMember.getClass());
System.out.println("findMember.id = " + findMember.getId());
System.out.println("findMember.username = " + findMember.getUsername());
tx.commit();
Hibernate:
/* insert hello.jpa.Member
*/ insert
into
Member
(id, createdBy, createdDate, lastModifiedBy, lastModifiedDate, age, description, locker_id, roleType, name)
values
(null, ?, ?, ?, ?, ?, ?, ?, ?, ?)
findMember = class hello.jpa.Member$HibernateProxy$yJgMgbkR
findMember.id = 1
Hibernate:
select
member0_.id as id1_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_.age as age6_4_0_,
member0_.description as descript7_4_0_,
member0_.locker_id as locker_10_4_0_,
member0_.roleType as roleType8_4_0_,
member0_.team_id as team_id11_4_0_,
member0_.name as name9_4_0_,
locker1_.id as id1_3_1_,
locker1_.name as name2_3_1_,
team2_.id as 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_.id
where
member0_.id=?
findMember.username = creator
프록시 특징
- 실제 클래스를 상속 받아서 만들어짐
- 하이버네이트가 내부적으로 상속받아서 만든다.
- 실제 클래스와 겉 모양이 같다.
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨(이론상)
- 프록시 객체는 실제 객체의 참조(target)을 보관
- 프록시 객체를 호출하면 프록시 객체는 실체 객체의 메소드 호출
프록시 객체의 초기화
- em.getReference()로 가져온 프록시 객체에서 getName()을 호출하면
- MemberProxy 객체에 target이 초기화가 안됐으므로, 영속성 컨텍스트에 Member 객체를 요청한다.
- 영속성 컨텍스트는 DB에서 조회를 하여 실제 Entity를 생성한다.
- 그리고 target이 실제 Member를 연결 시키므로 target.getName()을 호출해서 member.getName()을 반환한다.
- 프록시 객체에 target이 할당이 되면, 프록시 객체의 초기화 동작은 더 이상 없어도 된다.
Member member = em.getReference(Member.class, member.getId());
member.getName();
프록시 특징 정리
- 프록시 객체는 처음 사용할 때 한 번만 초기화
- 프록시 객체를 초기화 할 때, 프록시 객체가 실제 인티티로 바뀌는 것은 아님, 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
- 정확히는 프록시 객체의 target에 값이 실제 객체로 초기화 됨.
- 프록시 객체는 원본 엔티티를 상속받음. 따라서 타입 체크시 주의해야한다 (== 비교 실패, 대신 instance of 사용)
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
- 하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
Member find = em.find(Member.class, member1.getId());
Member reference = em.getReference(Member.class, member2.getId());
System.out.println("m1 == m2 : " + (m1 == m2));
find == reference : false
System.out.println("find : " + (find instanceof Member));
System.out.println("reference : " + (reference instanceof Member));
find : true
reference : true
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환. (반대도 마찬가지)
- 영속성 컨텍스트에 이미 객체가 존재한다면 굳이 프록시 객체를 통해 반환 할 필요가 없다( 굳이..?😨)
- JPA는 하나의 영속성 컨텍스트에서 조회하는 같은 엔티티 동일성을 보장한다.
- JPA가 기본적으로 제공하는 메커니즘 중 하나. 반복가능한 읽기(repeatable read) 제공
- 따라서, 아래의 코드에서 두 객체는 같다. em.getReference() 메소드를 사용해도 프록시 객체를 반환하지 않는다.
Member find = em.find(Member.class, member.getId());
Member reference = em.getReference(Member.class, member.getId());
System.out.println("find == reference : " + (find == reference)); // true
- 둘 다 getRefernect()로 가져오면?
- 결론은 둘다 프록시 객체이다. 여기서 중요한 점은 "같은 프록시 객체"이다
- JPA는 한 트랜잭션에서 조회하는 같은 엔티티의 동일성을 보장한다.
- 결론은 둘다 프록시 객체이다. 여기서 중요한 점은 "같은 프록시 객체"이다
Member reference1 = em.getReference(Member.class, member.getId());
Member reference2 = em.getReference(Member.class, member.getId());
System.out.println("reference1 == reference2 : " + (reference1 == reference2)); // true
- getRefernect()로 먼저 가져오고, find()로 실제 객체를 조회하면?
- 결론은 둘 다 같은 프록시 객체를 반환한다.
- 엥? find()는 실제 객체를 반환해야 하는거 아닌가?
- 이는 "JPA는 한 트랜잭션에서 조회하는 같은 엔티티의 동일성을 보장한다" 때문에
- 한 트랜잭션 내에서 reference == find를 true로 반환하기 위해 이렇게 동작한다.
- 참고로 JPA에서 내부적으로 다 처리를 해주기 때문에, 개발할때는 프록시던 진짜 객체던 중요하지는 않다. 그냥 멤버 조회하면서 개발 하면 된다
- 결론은 둘 다 같은 프록시 객체를 반환한다.
Member reference = em.getReference(Member.class, member.getId());
Member find = em.find(Member.class, member.getId());
System.out.println("reference == find : " + (reference == find)); // true
- 실무에서 많이 만나게 되는 문제
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 초기화 문제가 발생한다.
- 트랜잭션 범위 밖에서 프록시 객체를 조회할 때
- 하이버네이트는 org.hibernate.LazyInitializationException 예외가 발생
- em.detach(), em.close(), em.clear() 모두 위와 같은 에러가 발생
- 프록시 객체를 초기화 할 수 없기 때문에 더 이상 영속성 컨텍스트 도움을 받지 못한다.
- 트랜잭션 범위 밖에서 프록시 객체를 조회할 때
- 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 초기화 문제가 발생한다.
Member member = new Member();
member.setUsername("creator");
em.persist(member);
em.flush();
em.clear();
Member reference = em.getReference(Member.class, member.getId());
em.detach(reference);
//em.close도 동일
System.out.println("findMember.username = " + reference.getUsername());
tx.commit();
즉시 로딩과 지연 로딩
- 위 개념을 이해하기 위해 프록시에 대한 개념을 선행했다.
- 다시 처음 질문으로 돌아가 보자.
- Member를 조회할 때 Team을 함께 조회 해야할까?
- 비즈니스 로직에서 단순히 Member만 조회하는 거라면 Team이 연관관계 되어있더라도 같이 조회하는 것은 낭비이다.
- JPA는 위 질문을 지연로딩 LAZY를 사용해서 프록시로 조회하는 방법으로 해결한다.
코드
- Member와 Team이 다대일(@ManyToOne)로 매핑되어 있는 상황에서,
- @ManyToOne 에노테이션에 fetch 타입을 줄 수 있다.
- Fetch.LAZY
Member
@Entity
@Getter
@Setter
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String username;
private Integer age;
@Enumerated(EnumType.STRING)
private RoleType roleType;
@Lob
private String description;
// 패치 타입 LAZY 설정
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id", insertable = false, updatable = false)
private Team team;
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();
public void setTeam(Team team) {
this.team = team;
this.team.getMembers().add(this);
}
}
Main( Member를 조회하고 Team 객체의 조회를 한 시점에 쿼리가 나간다. )
Member member = new Member();
member.setUsername("memberA");
member.setCreatedBy("helloA");
member.setCreatedDate(LocalDateTime.of(2019,11,12,12,12));
em.persist(member);
Team team = new Team();
member.setTeam(team);
em.persist(team);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println("m = " + findMember.getTeam().getClass());
System.out.println("=============================================");
findMember.getTeam().getName(); // 이 시점에 쿼리가 나감
System.out.println("=============================================");
transaction.commit();
Hibernate:
select
member0_.MEMBER_ID as member_i1_3_0_,
member0_.createdBy as createdb2_3_0_,
member0_.createdDate as createdd3_3_0_,
member0_.lastModeifiedDate as lastmode4_3_0_,
member0_.lastModifiedBy as lastmodi5_3_0_,
member0_.TEAM_ID as team_id7_3_0_,
member0_.name as name6_3_0_
from
Member member0_
where
member0_.MEMBER_ID=?
m = class hellojpa.Team$HibernateProxy$dfWJNi0B
=============================================
Hibernate:
select
team0_.TEAM_ID as team_id1_7_0_,
team0_.name as name2_7_0_
from
Team team0_
where
team0_.TEAM_ID=?
=============================================
지연 로딩(LAZY)
- 연관 관계 어노테이션에 지연 로딩(LAZY)을 설정하면 아래 이미지와 같이 동작한다.
- 로딩되는 시점에 Lazy로 설정되어 있는 Team 엔티티는 프록시 객체로 가져온다.
- 로직에서 실제 객체를 사용할 때(Team) 초기화가 되고 DB에 쿼리가 나간다.
- getTeam()으로 Team을 조회하면 프록시 객체가 조회가 된다.
- getTeam().xxx()으로 Team 객체의 필드에 접근할 때 쿼리가 나간다.
즉시 로딩(EAGER)
- 즉시 로딩은 쉽게 말하면 연관된 객체를 한방 쿼리로 다 조회 하는 것이다.
- fetch 타입을 EAGER로 설정하면 즉시 로딩으로 설정된다.
- 실행 결과를 보면 프록시 객체가 아닌 실제 객체가 조회된다.
@Entity
@Getter
@Setter
public class Member extends BaseEntity {
...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "team_id", insertable = false, updatable = false)
private Team team;
...
}
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("memberA");
em.persist(member);
member.setTeam(team);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
System.out.println(findMember.getTeam().getClass());
System.out.println("TEAM NAME : " + findMember.getTeam().getName());
tx.commit();
실행 결과
Hibernate:
select
member0_.id as id1_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_.age as age6_4_0_,
member0_.description as descript7_4_0_,
member0_.locker_id as locker_10_4_0_,
member0_.roleType as roleType8_4_0_,
member0_.team_id as team_id11_4_0_,
member0_.name as name9_4_0_,
locker1_.id as id1_3_1_,
locker1_.name as name2_3_1_,
team2_.id as 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_.id
where
member0_.id=?
class hello.jpa.Team
TEAM NAME : teamA
프록시와 즉시 로딩 주의할 점
- 실무에서는 가급적 지연 로딩만 사용하자. (즉시 로딩은 사용하지 말자)
- 그럼 여기서 의문점이 생긴다. 한방 쿼리로 모두 가져오면 필요할 때 마다 쓸 수 있는거 아닌가?
- 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
- 만약 ManToOne 어노테이션이 5개가 있고 전부 EAGER로 설정됐다고 하자
- Join이 5개가 일어난다.. 실무에선 테이블이 훨씬 많을 것이다
- 즉시 로딩은 N+1 문제를 일으킨다.
- 실무에서 복잡한 쿼리를 많이 풀어내기 위해 JPQL을 사용한다.
- em.find()는 PK를 정해놓고 DB에서 가져오기 때문에 JPA 내부에서 최적화를 할 수 있다. (한방쿼리)
- 하지만, JPQL에선 입력 받은 query string이 그대로 SQL로 변환된다.
- "Slect m from Member m"이 문장은 Member만 조회한다.
- Member 객체를 보니깐
- 엉? Member 엔티티의 Team이 EAGER로 즉시 로딩 됐네?
- LAZY면 프록시를 넣으면 되지만, EAGER는 반환되는 시점에 이미 다 조회가 되어 있어야 한다.
- 따라서, Member를 다 가져오고 연간된 Team을 다시 가져와야 되는 일이 발생한다.
코드로 이해( 멤버가 2명, 팀은 2개이며 모든 멤버를 조회하는 경우)
Team team1 = new Team();
team1.setName("teamA");
em.persist(team1);
Team team2 = new Team();
team2.setName("teamB");
em.persist(team2);
Member member1 = new Member();
member1.setUsername("memberA");
em.persist(member1);
member1.setTeam(team1);
Member member2 = new Member();
member2.setUsername("memberB");
em.persist(member2);
member2.setTeam(team2);
em.flush();
em.clear();
List<Member> members = em
.createQuery("select m from Member m", Member.class)
.getResultList();
tx.commit();
- 실행 결과를 보면, 멤버를 가져오고,
- 멤버에 EAGER로 설정되어 있으므로 비어있는 Team을 채우기 위해 TEAM을 각각 쿼리를 날려서 가져온다.
- 만약 멤버가 100명이라고 생각해보자.
- Team을 채우기 위해 쿼리가 100개 날라갈 것이다.. 그래서 실무에서는 사용하지 말라는 것이다.
- 이제 N+1의 의미를 이해할 수 있다.
- 1은 Member를 조회하기 위해 날리는 쿼리이고, N은 예상치 못한 추가 쿼리가 N개가 발생한다는 것이다.
Hibernate:
/* select
m
from
Member m */ select
member0_.id as id1_4_,
member0_.createdBy as createdB2_4_,
member0_.createdDate as createdD3_4_,
member0_.lastModifiedBy as lastModi4_4_,
member0_.lastModifiedDate as lastModi5_4_,
member0_.age as age6_4_,
member0_.description as descript7_4_,
member0_.locker_id as locker_10_4_,
member0_.roleType as roleType8_4_,
member0_.team_id as team_id11_4_,
member0_.name as name9_4_
from
Member member0_
Hibernate:
select
team0_.id as 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_.id=?
Hibernate:
select
team0_.id as 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_.id=?
- 결론은 즉시 로딩보다는 가급적 그냥 지연 로딩 전략을 사용하자.
- "그러면 실무에서 모두 조회하고 싶은 경우가 대부분이여도 LAZY로 설정해야 하나?"
- 이를 해겨랗고자 JPQL의 fetch join을 통해서 해당 시점에 한방 쿼리로 가져와서 쓸 수 있다.
- 추가적으로 엔티티그래프, 어노테이션으로 푸는 방법, 배치 사이즈 설정으로도 해결할 수는 있다.
- 대부분 fetch join으로 해결한다.
- @ManyToOne, @OneToOne과 같이 @xxToOne은 기본적으로 EAGER 전략을 사용한다.
- 전부 LAZY로 설정해주자
지연 로딩 활용
- Member와 Team을 자주 함께 사용한다 -> 즉시 로딩
- Member와 Order는 가끔 사용한다 -> 지연 로딩
- Order와 Product는 자주 함꼐 사용한다 -> 즉시 로딩
- 위와 같이 설정해 놓고 쓸 수 있지만, 굉장히 이론적인 개념이고
- 실무에서는 다 LAZY로 쓰자. 즉시 로딩 사용하지 말자.
- JPQL fetch join이나, 엔티티 그래프 기능으로 해결하자.
- 즉시 로딩은 상상하지 못한 쿼리가 나간다.
영속성 전이 : CASCADE
- 영속성 전이는 즉시, 지연 로딩이나 연관관계 설정과 전혀 관계가 없다. ( 흔히들 착각한다!! )
- 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용한다.
- 예로 들면, 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장하고 싶은 경우
코드로 이해( 다대다 관계인 Parient, Child를 양방향 연관관계 매핑 )
Child
@Entity
@Getter
@Setter
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Parent parent;
}
Parent( 연관 관계 편의 메소드 추가 )
@Entity
@Getter
@Setter
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "parent")
private List<Child> children = new ArrayList<>();
public void addChild(Child child) {
this.children.add(child);
child.setParent(this);
}
}
- 원래 였으면 Main에서 저장할 때, em.persist를 3번 해야한다.
- 그런데, 개발 하다가 Parent가 Child를 관리해줬으면 한다.
- 이런 상황에서 CASCADE를 활용하면 된다.
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);
tx.commit();
- Parent의 @OneToMany에서 CASCADE 옵션 추가
@Entity
@Getter
@Setter
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) //cascade 추가
private List<Child> children = new ArrayList<>();
public void addChild(Child child) {
this.children.add(child);
child.setParent(this);
}
}
- 이전 상황과 달리 Parent만 em.persist
- 정상적으로 insert 쿼리 3개가 나간다
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
tx.commit();
Hibernate:
/* insert cascade.Parent
*/ insert
into
Parent
(id, name)
values
(null, ?)
Hibernate:
/* insert cascade.Child
*/ insert
into
Child
(id, name, parent_id)
values
(null, ?, ?)
Hibernate:
/* insert cascade.Child
*/ insert
into
Child
(id, name, parent_id)
values
(null, ?, ?)
CACADE 주의사항
- 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다.
- 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.
- @OneToMany(mappedBy = ) 선언된 쪽에 다 걸어야 되나?
- 아니다.
- 단독 부모가 자식들을 관리할 때 의미가 있다.
- 게시판을 예로 들면, 하나의 게시글(부모)에서 첨부파일들의 경로(자식)을 관리 할 때 유용.
- 하지만, 해당 첨부파일들이 게시글 말고도 다른 엔티티들과 연관 관계가 있으면 절대 사용하면 안된다.
- 단일 엔티티에 완전히 종속적인 Child를 관리할 때 유용하게 쓰자. 둘의 생명주기 (라이프 사이클)이 같기 때문에 가능하다.
CASCADE의 종류 (ALL, PERSIST, REMOVE만 주로 사용)
- ALL - 모두 적용
- 모든 라이프 사이클을 맞춰야 하거나 삭제가 위험할 때
- PERSIST - 영속
- 저장할 때만 사용
- REMOVE - 삭제
- MERGE
- REFRESH
- DETACH
고아 객체
- 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제한다.
- 마찬가지로 @OneToMany Child 컬렉션 쪽에 orpahnRemoval = true 옵션을 준다.
@Entity
@Getter
@Setter
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> children = new ArrayList<>();
public void addChild(Child child) {
this.children.add(child);
child.setParent(this);
}
}
- 자식 엔티티를 컬렉션에서 삭제하면
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.getChildren().remove(0);
tx.commit();
- 자동으로 삭제(DELETE)가 된다.
...
Hibernate:
select
parent0_.id as id1_8_0_,
parent0_.name as name2_8_0_
from
Parent parent0_
where
parent0_.id=?
Hibernate:
select
children0_.parent_id as parent_i3_2_0_,
children0_.id as id1_2_0_,
children0_.id as id1_2_1_,
children0_.name as name2_2_1_,
children0_.parent_id as parent_i3_2_1_
from
Child children0_
where
children0_.parent_id=?
Hibernate:
/* delete cascade.Child */
delete
from
Child
where
id=?
고아 객체 사용 주의 사항
- 이것 또한 마찬가지로 참조하는 곳이 하나일 때 사용해야 한다.
- 특정 엔티티가 단도긍로 소유할 때 사용 가능하다. 여러 엔티티와 관계가 연관되어 있다면 사용하지 말자.
- @OneToOne, @OneToMany -> @OneToXXX 에서만 사용 가능하다.
- 참고
- 개념적으로 부모를 제거하면 자식은 고아가 된다.
- 따라서 고아 객체 기능을 활성화하면 부모를 제거하면 자식도 제거가 된다.
- 이것은 cascade = CascadeType.ALL를 옵션에서 제거했지만 CascadeType.REMOVE 처럼 동작한다.
코드로 이해하기 ( child 대신 parent를 지운 경우 )
@Getter
@Setter
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<>();
public void addChild(Child child) {
this.children.add(child);
child.setParent(this);
}
}
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());
em.remove(findParent);
tx.commit();
- delete 쿼리가 3개 나간다.
...
Hibernate:
/* delete cascade.Child */ delete
from
Child
where
id=?
Hibernate:
/* delete cascade.Child */ delete
from
Child
where
id=?
Hibernate:
/* delete cascade.Parent */ delete
from
Parent
where
id=?
영속성 전이 + 고아 객체, 생명주기
- CascadeType.ALL과 orphanRemoval = true를 같이 쓰면 어떻게 될까?
- 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있따.
- 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.
Reference
자바 ORM 표준 JPA 프로그래밍