목차
- 프록시
- 즉시 로딩과 지연 로딩
- 지연 로딩 활용
- 영속성 전이: CASCADE
- 고아 객체
- 영속성 전이 + 고아 객체, 생명주기
- 질문으로 부터 프록시에 대한 학습을 시작한다.
- Member 엔티티를 조회할 때 Team도 함께 조회해야 할까?
- 실제로 필요한 비즈니스 로직에 따라 다르다.
- 비즈니스 로직에서 필요하지 않을 때가 있는데, 항상 Team을 함께 가져와서 사용할 필요는 없다.
- 낭비가 발생하게 된다.
- JPA는 이 낭비를 하지 않기 위해, 지연로딩과 프록시라는 개념으로 해결한다.
-
지연 로딩을 이해하려면, 프록시의 개념에 대해서 명확하게 이해해야 한다.
-
JPA에서 em.find() 말고, em.getReference()라는 메서드도 제공 된다.
-
em.find() 는 DB를 통해서 실제 엔티티 객체를 조회하는 메서드이고
-
em.getReference() 는 DB의 조회를 미루는 가짜(프록시) 엔티티 객체를 조회하는 메서드이다.
-
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; @ManyToOne @JoinColumn(name = "team_id") private Team team; @OneToOne @JoinColumn(name = "locker_id") private Locker locker; @OneToMany(mappedBy = "member") private List<MemberProduct> memberProducts = new ArrayList<>(); }
-
em.find()로 멤버를 조회하면 아래와 같이 데이터베이스에 쿼리가 바로 나간다.
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)을 보관한다.
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.
Member member = em.getReference(Member.class, member.getId());
member.getName();
- em.getReference()로 프록시 객체를 가져온 다음에, getName() 메서드를 호출 하면
- MemberProxy 객체에 처음에 target 값이 존재하지 않는다. JPA가 영속성 컨텍스트에 초기화 요청을 한다.
- 영속성 컨텍스트가 DB에서 조회해서
- 실제 Entity를 생성해준다.
- 그리고 프록시 객체가 가지고 있는 target(실제 Member)의 getName()을 호출해서 결국 member.getName()을 호출한 결과를 받을 수 있다.
- 프록시 객체에 target이 할당 되고 나면, 더이상 프록시 객체의 초기화 동작은 없어도 된다.
실제로는 위와 같이 프록시 객체가 동작 한 것이다.
-
프록시 객체는 처음 사용할 때 한 번만 초기화 된다.
-
프록시 객체를 초기화 할 때, 프록시 객체가 실제로 엔티티로 바뀌는 것은 아니다.
- 초기화 되면 프록시 객체를 통해서 실제 엔티티에 접근 가능하다.
- 정확히 말하면 target에 값이 채워지는 것 뿐이다.
- em.getReference()로 조회한 클래스를 getClass()로 보면, HibernateProxy 클래스였던 것을 위에서 학습했다.
-
(심화)프록시 객체는 원본 엔티티를 상속 받는다고 했다. 프록시 객체와 원본 객체가 타입이 다르다. 타입 체크시 주의해야 한다.
-
== 비교 실패한다. jpa에서 타입 비교는 웬만하면, instanceOf를 사용해야 한다.
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는 하나의 영속성 컨텍스트에서 조회하는 같은 엔티티의 동일성을 보장한다.
- JPA가 기본적으로 제공하는 매커니즘 중 하나이다. 반복가능한 읽기(repeatable read) 제공
-
따라서, 아래의 코드에서 두 객체는 같다. em.gerReference()로 프록시 객체를 굳이 가져오지 않는다.
Member find = em.find(Member.class, member.getId()); Member reference = em.getReference(Member.class, member.getId()); System.out.println("find == reference : " + (find == reference)); // true
-
(더 심화) 그렇다면 둘다 getReference() 로 가져오면?!
-
둘 다, 프록시 객체이다. 근데, 같은 프록시 객체다. 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
-
그러면, getReference()로 먼저 가져오고, find()로 실제 객체를 조회하면?
-
하나는 프록시 객체, 하나는 당연히 find니까 실제 객체이지 않을까?
-
결론 부터 말하면 둘 다 같은 프록시 객체를 반환한다.
-
한 트랜잭션에서 조회하는 같은 엔티티의 동일성을 보장 하기 위해서.
-
한 트랜잭션 내에서 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();
org.hibernate.LazyInitializationException: could not initialize proxy [hello.jpa.Member#1] - no Session at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:169) at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:309) at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45) at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95) at hello.jpa.Member$HibernateProxy$bCLaGKb1.getUsername(Unknown Source) at JpaMain.main(JpaMain.java:35)
-
-
-
프록시 확인을 도와주는 Util성 메소드들이 있다.
-
프록시 인스턴스의 초기화 여부를 직접 확인
-
PersistenceUnitUtil.isLoaded(Object entity);
// 엔티티 매니저 팩토리로 부터 get boolean isLoaded = emf.getPersistenceUnitUtil().isLoaded(referenceMember);
-
-
프록시 클래스 확인 방법
- entity.getClass().getName() 출력 (..javasist.. or HibernateProxy...)
-
프록시 강제 초기화
-
org.hibernate.Hibernate.initialize(entity);
Hibernate.initialize(referenceMamber);
-
-
참고로
-
JPA 표준은 강제 초기화 메서드(initialize)가 없다. Hibernate가 지원한다.
-
그냥 프록시 객체에서 getXXX() 호출해서 강제로 초기화 한다.
-
- 프록시 학습 처음에 했던 질문. Member를 조회할 때 Team도 함께 조회 해야 할까?
- 비즈니스 로직에서 단순히 멤버 로직만 사용하는데 함께 조회하면, 아무리 연관관계가 걸려있다고 해도 손해이다.
- JPA는 이 문제를 지연로딩 LAZY를 사용해서 프록시로 조회하는 방법으로 해결 한다.
-
Member와 Team 사이가 다대일 @ManyToOne 관계로 매핑되어 있는 상황에서,
-
@ManyToOne 어노테이션에 fetch 타입을 줄 수 있다.
-
FetchType.LAZY
@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; @OneToOne @JoinColumn(name = "locker_id") private Locker locker; @OneToMany(mappedBy = "member") private List<MemberProduct> memberProducts = new ArrayList<>(); public void changeTeam(Team team) { this.team = team; this.team.getMembers().add(this); } }
-
메인 함수에서 팀과 멤버를 저장하고 조회 해보자.
- Member를 조회하고, Team 객체의 클래스를 확인해보면 Proxy 객체가 조회 된다.
Team team = new Team(); team.setName("teamA"); em.persist(team); Member member = new Member(); member.setUsername("memberA"); em.persist(member); member.changeTeam(team); em.flush(); em.clear(); Member findMember = em.find(Member.class, member.getId()); System.out.println(findMember.getTeam().getClass());
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_ from Member member0_ left outer join Locker locker1_ on member0_.locker_id=locker1_.id where member0_.id=? class hello.jpa.Team$HibernateProxy$e97rdqZR // 프록시 객체
-
팀의 이름을 출력해보자
-
이 시점에. 실제로 팀 객체의 조회가 필요한 시점에 쿼리가 나간다.
Team team = new Team(); team.setName("teamA"); em.persist(team); Member member = new Member(); member.setUsername("memberA"); em.persist(member); member.changeTeam(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());
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_ from Member member0_ left outer join Locker locker1_ on member0_.locker_id=locker1_.id where member0_.id=? class hello.jpa.Team$HibernateProxy$z4JtUeLD // 프록시 객체 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=? TEAM NAME : teamA
-
- 내부 매커니즘은 위의 그림과 같다.
- 로딩되는 시점에 Lazy 로딩 설정이 되어있는 Team 엔티티는 프록시 객체로 가져온다.
- 후에 실제 객체를 사용하는 시점에(Team을 사용하는 시점에) 초기화가 된다. DB에 쿼리가 나간다.
- getTeam()으로 Team을 조회하면 프록시 객체가 조회가 된다.
- getTeam().getXXX()으로 팀의 필드에 접근 할 때, 쿼리가 나간다.
- 이런 경우 LAZY 로딩을 사용한다면, SELECT 쿼리가 따로따로 2번 나간다.
- 네트워크를 2번 타서 조회가 이루어 진다는 이야기이다. 손해다.
- 이때는 즉시 로딩(EAGER) 전략을 사용해서 함께 조회하면 된다.
-
fetch 타입을 EAGER로 설정하면 된다.
-
대부분의 JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 함께 조회하려고 한다.
-
이렇게 하면, 실제 조회할 때 한방 쿼리로 다 조회해온다.(실제 Team을 사용할 때 쿼리 안나가도 된다.)
-
실행 결과를 보면 Team 객체도 프록시 객체가 아니라 실제 객체이다.
@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.changeTeam(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
-
실무에서는 가급적 지연 로딩만 사용하다. 즉시 로딩 쓰지 말자.
- JPA 구현체도 한번에 가저오려고 하고, 한번에 가져와서 쓰면 좋지 않나?
-
즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다.
- @ManyToOne이 5개 있는데 전부 EAGER로 설정되어 있다고 생각해보자.
- 조인이 5개 일어난다. 실무에선 테이블이 더 많다.
-
즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
-
실무에서 복잡한 쿼리를 많이 풀어내기 위해서 뒤에서 학습할 JPQL을 많이 사용한다.
-
em.find()는 PK를 정해놓고 DB에서 가져오기 때문에 JPA 내부에서 최적화를 할 수 있다.(한방 쿼리)
-
하지만, JPQL에선 입력 받은 query string이 그대로 SQL로 변환된다.
- "select m from Member m" 이 문장으로 당연히 Member만 SELECT 하게 된다.
- MEMBER를 쭉 다 가져와서 보니까
-
어 근데, Member 엔티티의 Team의 fetchType이 EAGER네?
- LAZY면 프록시를 넣으면 되겠지만, EAGER는 반환하는 시점에 다 조회가 되어 있어야 한다.
- 따라서, Member를 다 가져오고 나서, 그 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.changeTeam(team1); Member member2 = new Member(); member2.setUsername("memberB"); em.persist(member2); member2.changeTeam(team2); em.flush(); em.clear(); List<Member> members = em .createQuery("select m from Member m", Member.class) .getResultList(); tx.commit();
-
실행 결과를 보면,
-
일단 멤버를 조회해서 가져온다.
-
그리고 나서 Member들의 Team이 비어있으니까 채워서 반환시키기 위해서 TEAM을 각각 쿼리 날려서 가져온다.
-
멤버가 수천 수만명이라고 생각하면...... 아찔해진다.
-
N + 1의 문제의 의미는
- 아래 처럼 쿼리를 1개 날렸는데, 그것 때문에 추가 쿼리가 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 로딩 전략을 가져가자.
- 근데 실무에서 대부분 멤버 팀을 함께 사용하는 경우가 있는데, 그러면 LAZY로 해놓고 계속 쿼리 두방 날려서 조회 해올까요?
-
이런 경우를 위해서 JPQL의 fetch join 을 통해서 해당 시점에 한방 쿼리로 가져와서 쓸 수 있다.
- 추가적으로 엔티티그래프와 어노테이션으로 푸는 방법, 배치 사이즈 설정으로 해결하는 방법이 있다.
- 대부분 fetch join으로 해결 한다.
-
-
@ManyToOne, @OneToOne과 같이 @XXXToOne 어노테이션들은 기본이 즉시 로딩(EAGER) 이다.
- 꼭 LAZY로 명시적으로 설정해서 사용하자
-
@OneToMany와 @ManyToMany는 기본이 지연 로딩(LAZY)이다.
- Member와 Team을 자주 함께 사용한다 -> 즉시 로딩
- Member와 Order는 가끔 사용한다 -> 지연 로딩
- Order와 Product는 자주 함꼐 사용한다 -> 즉시 로딩
- 위와 같이 설정해 놓고 쓸 수 있지만, 굉장히 이론적인 개념이고
- 실무에서는 다 LAZY로 쓰자. 즉시 로딩 사용하지 말자.
- JPQL fetch join이나, 엔티티 그래프 기능으로 해결하자.
- 즉시 로딩은 상상하지 못한 쿼리가 나간다.
- 영속성 전이는 즉시,지연 로딩이나 연관관계 설정과 전혀 관계가 없다.
- 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용한다.
- 예를 들면,
- 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장하고 싶은 상황에서.
-
다대일 관계인 Child와 Parent를 만들고, 양방향 연관관계 매핑을 해준다.
-
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 따로따로.
-
그런데, 개발 하다보면 Parent가 Child를 관리해줬으면 한다.
-
이런 상황에서 JPA의 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만 저장해보자. 결과는?
-
정상적으로 insert 쿼리 3개 나간다. Parent, Child * 2
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, ?, ?)
-
- 영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다.
- 엔티티를 영속화할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.
- @OneToMany(mappedBy = ) 선언된 쪽에 다 걸어야 되나?
- 아니다.
- 단독 부모가 자식들을 관리할 때 의미가 있다.
- 게시판을 예로 들면, 하나의 게시글(부모)에서 첨부파일들의 경로(자식)을 관리 할 때 유용.
- 하지만, 해당 첨부파일들이 게시글 말고도 다른 엔티티들과 연관 관계가 있으면 절대 사용하면 안된다.
- 단일 엔티티에 완전히 종속적인 Child를 관리할 때 유용하게 쓰자. 둘의 라이프 사이클이 같기 때문에 가능하다.
- ALL - 모두 적용
- 모든 라이프 사이클을 맞춰야 할 때, 삭제가 위험할 때
- PERSIST - 영속
- 저장할 때만 사용
- REMOVE - 삭제
- MERGE
- REFRESH
- DETACH
-
부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제한다.
-
마찬가지로 @OneToManyChild 컬렉션쪽에 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 에서만 사용 가능하다.
-
참고
-
개념적으로 부모를 제거하면 자식은 고아가 된다.
-
따라서 고아 객체 제거 기능을 활성화하면, 부모를 제거할 때 자식도 함께 제거 된다.
-
이것은 cascade = CascadeType.ALL를 옵션에서 제거했지만, CascadeType.REMOVE 처럼 동작한다.
-
실제로 바로 위의 예제에서 0번째 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방 나간다. Child 2개 지우고, Parent 지운다.
... 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=?
-
- 그러면, CacadeType.ALL과 orphanRemoval = true를 같이 쓰면 어떻게 될까?
- 스스로 생명주기를 관리하는 엔티티는
- em.persist()로 영속화하고
- em.remove()로 제거한다.
- 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.
- 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.