소스
https://github.com/rkwhr0010/jpa
프록시
엔티티를 조회할 때 반드시 연관 엔티티들이 사용되진 않는다.
JPA는 사용하는 시점에 조회한다. 사용하는 시점에 조회흘 수행하려면, 객체의 메소드에 대한 흐름을 제어해야 한다. JPA는 내부적으로 프록시를 이용해 지연 로딩을 수행한다.
프록시 특징
- 프록시 객체는 처음 사용할 때 한 번만 초기화
- 프록시는 원본 엔티티를 상속한 객체로 타입 체크 주의
- 영속성 컨텍스트에 이미 찾는 엔티티가 있으면, em.getReference()를 호출해도 원본 객체를 반환
- 초기화는 영속성 컨텍스트의 도움을 받는다. 따라서 준영속 상태가 되서 첫 사용을 하면 예외가 발생한다.
@Entity @Data public class Member { @Id @GeneratedValue @Column(name = "member_id") private Long id; @ManyToOne private Team team; private String name; } |
@Entity @Data public class Team { @Id @GeneratedValue @Column(name = "member_id") private Long id; private String name; } |
private static void find() { logic(em -> { Member member = em.find(Member.class, 1L); System.out.println("#########################################"); System.out.println(member.getClass().getSuperclass()); System.out.println(member.getClass()); // Member 프록시를 반환 Member reference = em.getReference(Member.class, 1L); // 첫 사용 한 번만 초기화 수행 reference.getTeam(); System.out.println("#########################################"); System.out.println(reference.getClass().getSuperclass()); System.out.println(reference.getClass()); }); } |
private static void find2() { logic(em -> { // 아무것도 수행 안함 Member reference = em.getReference(Member.class, 1L); // 첫 사용 한 번만 초기화 수행 reference.getTeam(); System.out.println("#########################################"); System.out.println(reference.getClass().getSuperclass()); System.out.println(reference.getClass()); Member member = em.find(Member.class, 1L); System.out.println("#########################################"); System.out.println(member.getClass().getSuperclass()); System.out.println(member.getClass()); }); } |
private static void find3() { logic(em -> { Member reference = em.getReference(Member.class, 1L); //준영속 em.detach(reference); // 첫 사용 한 번만 초기화 수행 reference.getTeam(); System.out.println("#########################################"); System.out.println(reference.getClass().getSuperclass()); System.out.println(reference.getClass()); }); } |
프록시와 식별자
private static void find4() { logic(em -> { Member reference = em.getReference(Member.class, 1L); System.out.println("#########################################"); System.out.println(emf.getPersistenceUnitUtil().isLoaded(reference)); // 첫 사용 한 번만 초기화 수행 reference.getTeam(); System.out.println("#########################################"); System.out.println(emf.getPersistenceUnitUtil().isLoaded(reference)); }); } |
즉시 로딩과 지연 로딩
- 즉시 로딩
연관된 엔티티도 함께 조회 - 지연 로딩
연된된 엔티티 사용할 때 조회
프록시는 연관된 엔티티를 지연 로딩할 때 쓰인다.
즉시 로딩
즉시 로딩은 연관된 엔티티를 함께 보기 위해 조인 쿼리를 사용한다.
null 제약조건과 JPA 조인 전략
위 sql을 보면 외부 조인이다. 이유는 Member 테이블에 team_id 는 null을 허용하기 때문이다.
@JoinColumn nullable 속성에 이 외래 키는 null을 허용하지 않는다는 제약을 주면, 내부 조인 사용해 외부 조인보다 더 좋은 성능을 기대할 수 있다.
동일한 결과
JPA는 선택적 관계면 외부조인을 필수적 관계면 내부 조인을 사용한다.
지연 로딩
즉시 로딩, 지연 로딩 정리
지연로딩
연관된 엔티티를 프록시로 조회
연관된 엔티티는 실제 사용 시 DB에 질의한다
즉시로딩
연관된 엔티티도 조인을 통해 한 번에 조회
지연 로딩 활용
JPA 기본 페치 전략
연관된 엔티티가 하나면 즉시로딩
컬렉션이면 지연로딩
추천하는 방법은 모든 연관관계에 지연 로딩을 사용하고, 필요한 곳만 즉시 로딩을 사용해 최적화한다.
SQL로 개발하면, 시도조차 못하는 것이 지연로딩 이를 적극적으로 이용하자
컬렉션에 FetchType.EAGER 사용 시 주의점
컬렉션 하나 이상 즉시 로딩을 피한다.
엔티티 속 컬렉션은 DB로 보면 일대다 조인을 의미한다.
일대다 조인은 데이터가 다 쪽 로우 수 만큼 증가한다.
컬렉션 이 두 개 있다면, C1 * C2 만큼 데이터를 반환할 수 있다.
JPA는 이렇게 조호된 결과를 메모리에서 필터링한다.
절대 두 개 이상 컬렉션을 즉시 로딩하지 않는다.
컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.
다대일의 경우 null 제약 조건에 따라 null 허용이면, 외부조인
null 불가이면, 항상 외래 키가 있다는 뜻이고, 외래 키는 참조 테이블에 키가 있다는 것을 의미한다. 따라서 내부조인을 한다.
일대다의 경우 컬렉션 즉시 로딩은 항상 외부 조인을 사용한다.
영속성 전이: CASCADE
@Entity @Data public class Parent { @Id @GeneratedValue @Column(name = "parent_id") private Long id; @OneToMany(mappedBy = "parent") private List<Child> childs = new ArrayList<>(); } |
@Entity @Data public class Child { @Id @GeneratedValue @Column(name = "child_id") private Long id; @ManyToOne @JoinColumn(name = "parent_id") private Parent parent; } |
Parent p1 = new Parent(); em.persist(p1); Child c1 = new Child(); c1.setParent(p1); // 자식 -> 부모 연관관계 p1.getChilds().add(c1); // 부모 -> 자식 그래프 탐색용 em.persist(c1); Child c2 = new Child(); c2.setParent(p1); // 자식 -> 부모 연관관계 p1.getChilds().add(c2); // 부모 -> 자식 그래프 탐색용 em.persist(c2); |
JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
영속성 전이: 저장
@Entity @Data public class Parent { @Id @GeneratedValue @Column(name = "parent_id") private Long id; // @OneToMany(mappedBy = "parent") @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST) private List<Child> childs = new ArrayList<>(); } |
부모를 영속화할 때 연관된 자식들도 영속화한다.
Parent p1 = new Parent(); em.persist(p1); Child c1 = new Child(); c1.setParent(p1); // 자식 -> 부모 연관관계 p1.getChilds().add(c1); // 부모 -> 자식 그래프 탐색용 Child c2 = new Child(); c2.setParent(p1); // 자식 -> 부모 연관관계 p1.getChilds().add(c2); // 부모 -> 자식 그래프 탐색용 |
영속성 전이가 되어 Child도 영속화된 것을 볼 수 있다.
영속성 전이 자체는 연관관계 매핑과 아무런 관계가 없다. 단순히 영속화 편리성을 제공한다.
영속성 전이: 삭제
Caused by: java.sql.SQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`jpa`.`Child`, CONSTRAINT `FKlh67j1n7x7gt59u0pbkwqh6o6` FOREIGN KEY (`parent_id`) REFERENCES `Parent` (`parent_id`)) |
그냥 삭제 시도 시 외래 키 제약 조건 때문에 삭제를 못한다.
외래 키 제약 조건을 따져 순서에 맞게 제거가 된다.
영속성 전이는 플러시 시점에 전이가 발생한다.
고아 객체
부모 엔티티와 연관관계가 끊어진 자식 엔티티를 삭제하는 기능을 고아 객체 제거라 한다.
부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제된다.
@Entity @Data public class Parent { @Id @GeneratedValue @Column(name = "parent_id") private Long id; @OneToMany(mappedBy = "parent", orphanRemoval = true, cascade = CascadeType.PERSIST) private List<Child> childs = new ArrayList<>(); } |
@Entity @Data public class Child { @Id @GeneratedValue @Column(name = "child_id") private Long id; @ManyToOne @JoinColumn(name = "parent_id") private Parent parent; } |
// 저장 Parent p1 = new Parent(); em.persist(p1); for (int i = 0; i < 10; i++) { Child c1 = new Child(); c1.setParent(p1); // 자식 -> 부모 연관관계 p1.getChilds().add(c1); // 부모 -> 자식 그래프 탐색용 em.persist(c1); } |
Parent p1 = em.find(Parent.class, 1L); p1.getChilds().clear(); |
영속성 전이 + 고아 객체, 생명주기
부모 엔티티를 통해 자식 엔티티 생명 주기를 관리할 수 있게 된다.
public class Main { static EntityManagerFactory emf = Persistence.createEntityManagerFactory("studyjpa"); public static void main(String[] args) { logic(save()); logic(remove()); emf.close(); } private static Consumer<EntityManager> remove() { return em -> { Parent p1 = em.find(Parent.class, 1L); p1.getChilds().clear(); }; } private static Consumer<EntityManager> save() { return em -> { Parent p1 = new Parent(); for (int i = 0; i < 10; i++) { Child c1 = new Child(); c1.setParent(p1); p1.getChilds().add(c1); } em.persist(p1); }; } static void logic(Consumer<EntityManager> logic) { EntityManager em = emf.createEntityManager(); try { em.getTransaction().begin(); logic.accept(em); em.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); em.getTransaction().rollback(); } finally { em.close(); } } } |
실전 예제
연관관계 관리
나머지 예제 그대로 바뀐 파일 코드만
@Entity @Table(name = "orders") @Data public class Order extends BaseEntity{ @Id @GeneratedValue @Column(name = "order_id") private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) private List<OrderItem> orderItems = new ArrayList<>(); @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) @JoinColumn(name = "delivery_id") private Delivery delivery; private LocalDateTime orderDate; @Enumerated(EnumType.STRING) private OrderStatus status; // 연관관계 편의 메소드 public void setMember(Member member) { if (this.member != null) { this.member.getOrders().remove(this); } this.member = member; member.getOrders().add(this); } public void addOrderItem(OrderItem orderItem) { orderItems.add(orderItem); orderItem.setOrder(this); } public void setDelivery(Delivery delivery) { this.delivery = delivery; delivery.setOrder(this); } } |
Delivery delivery = new Delivery(); OrderItem orderItem1 = new OrderItem(); OrderItem orderItem2 = new OrderItem(); Order order = new Order(); order.setDelivery(delivery);; order.addOrderItem(orderItem1); order.addOrderItem(orderItem2); em.persist(order); |
'개발 > JPA' 카테고리의 다른 글
12 스프링 데이터 JPA (1) | 2024.06.03 |
---|---|
10 객체지향 쿼리 언어 (0) | 2024.05.27 |
08 프록시와 연관관계 관리 (1) | 2024.05.13 |
07 고급 매핑 (0) | 2024.05.06 |
06 다양한 연관관계 매핑 (0) | 2024.04.14 |