엔티티 설계시에는 가급적 Setter을 사용안하는게 좋다.
그리고 모든 연관관계는 지연로딩으로 설정을 해줘야 한다.
- 즉시로딩( EAGER )은 예측이 어렵고, 어떤 SQL이 실행될지 추적하기 어렵다. 특히 JPQL을 실행할 때 N+1 문제가 자주 발생한다.
- 실무에서 모든 연관관계는 지연로딩( LAZY )으로 설정해야 한다.
- 연관된 엔티티를 함께 DB에서 조회해야 하면, fetch join 또는 엔티티 그래프 기능을 사용한다.
- @XToOne(OneToOne, ManyToOne) 관계는 기본이 즉시로딩이므로 직접 지연로딩으로 설정해야 한다.
최악의 경우를 살펴보자.
아래에서 Member Order 연관 Order OrderItem이 연관 이렇게 다 연관이 되어있다 하자.
하나를 가져오면 다 연관되어 있는 애들을 DB에서 다 끌고 가져오기 때문에 절대로 쓰면 안된다.
그래서 지연로딩으로 모든 연관관계를 설정을 해주고(fetch join) 또는 엔티티 그래프 기능을 사용한다.
- 실무에서 모든 연관관계는 지연로딩( LAZY )으로 설정해야 한다.
- 연관된 엔티티를 함께 DB에서 조회해야 하면, fetch join 또는 엔티티 그래프 기능을 사용한다.
- @XToOne(OneToOne, ManyToOne) 관계는 기본이 즉시로딩이므로 직접 지연로딩으로 설정해야 한다.
Order와 Member을 만약 아래와 같이 세팅을 해놓는다고 하자.
Order을 조회하는데 fetch를 아래와 같이 작성한다고 하자.
Order을 조회할 때 member을 join을 해서 같이 가져온다.(query를 한번에)
(Many to One은 기본 Fetch가 EAGER One to One도 마찬가지 한개를 찌르는 거는 다 EAGER)
(만약 EAGER로 지정안해놓으면 연관된 쿼리가 무수히 많이 나가게 된다.)
그래서 ~ to One에서는 LAZY를 쓰면 안된다.
진행중인 프로젝트에서 command shift f를 눌러서 ~(Many, One) to One을 모두 LAZY로 바꿔줘야 한다.
// ManyToOne.java
public @interface ManyToOne {
/**
* (Optional) The entity class that is the target of
* the association.
*
* <p> Defaults to the type of the field or property
* that stores the association.
*/
Class targetEntity() default void.class;
/**
* (Optional) The operations that must be cascaded to
* the target of the association.
*
* <p> By default no operations are cascaded.
*/
CascadeType[] cascade() default {};
/**
* (Optional) Whether the association should be lazily
* loaded or must be eagerly fetched. The EAGER
* strategy is a requirement on the persistence provider runtime that
* the associated entity must be eagerly fetched. The LAZY
* strategy is a hint to the persistence provider runtime.
*/
FetchType fetch() default EAGER;
/**
* (Optional) Whether the association is optional. If set
* to false then a non-null relationship must always exist.
*/
boolean optional() default true;
}
(One to Many은 기본이 LAZY 그러므로 그냥 공란으로 놔둬도 LAZY로 인식한다.)
두개가 다르다.
// OneToMany
public @interface OneToMany {
/**
* (Optional) The entity class that is the target
* of the association. Optional only if the collection
* property is defined using Java generics.
* Must be specified otherwise.
*
* <p> Defaults to the parameterized type of
* the collection when defined using generics.
*/
Class targetEntity() default void.class;
/**
* (Optional) The operations that must be cascaded to
* the target of the association.
* <p> Defaults to no operations being cascaded.
*
* <p> When the target collection is a {@link java.util.Map
* java.util.Map}, the <code>cascade</code> element applies to the
* map value.
*/
CascadeType[] cascade() default {};
/** (Optional) Whether the association should be lazily loaded or
* must be eagerly fetched. The EAGER strategy is a requirement on
* the persistence provider runtime that the associated entities
* must be eagerly fetched. The LAZY strategy is a hint to the
* persistence provider runtime.
*/
FetchType fetch() default LAZY;
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
// JPQL select o From order o; -> SQL로 그대로 번역이 된다. 문제가 된다.
// select * from order
// order을 100개를 조회했다고 하자.
// 100개를 조회했는데 보니까 order만 날라왔다.(member가 FetchType.EAGER로 되어있네?)
// 단건 쿼리가 100개가 날라가게 된다....
// => n + 1 문제(첫번째 쿼리가 가져온 결과가 100개면 100(result) + 1(order)이 되어서)
@ManyToOne(fetch = FetchType.EAGER)
// EAGER 이름에서 알 수 있듯이 뭘 꼭 하겠다. => 즉 member을 꼭 가져오겠다라고 생각이 든다.
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
그리고 Member에 보면 아래와 같이 초기화를 할지(이게 낫다.)
private List<Order> orders = new ArrayList<>();
아래와 같이 초기화를 할 지 고민이 생길 수 있는데.
@OneToMany(mappedBy = "member")
private List<Order> orders;
public Member() {
orders = new ArrayList<>();
}
아래를 보면 member 객체를 생성을 하는데
가급적이면 위에서 private List<Order> orders = new ArrayList<>(); 이 부분을 밖으로 꺼내지도 말고(수정가능성)
변경도 하면 안된다. 객체 생성할 때 한번 넣어놓고 바꾸지 말자.(컬렉션을 필드에다 바로 초기화하는 것이 제일 좋다.)
Member member = new Member();
System.out.println(member.getOrders().getClass());
em.persist(team);
System.out.println(member.getOrders().getClass());
//출력 결과
class java.util.ArrayList
// hibernate가 추적할 수 있는 본인의 PersistentBag을 이용해서 바꿔버린다.
class org.hibernate.collection.internal.PersistentBag
// 만약 set해서 collection을 바꾼다? 그러면 hibernate가 원하는 메커니즘으로 안돌아간다.
다음으로 테이블 & 컬럼명을 생성하는 전략을 보자.
- 스프링 부트에서 하이버네이트 기본 매핑 전략을 변경해서 실제 테이블 필드명은 다름
member table명을 default로 아무것도 안하게 되면 어떻게 할 건지에 대한 전략이 있다.
- 하이버네이트 기존 구현: 엔티티의 필드명을 그대로 테이블의 컬럼명으로 사용 ( SpringPhysicalNamingStrategy )
-
스프링 부트 신규 설정 (엔티티(필드) 테이블(컬럼))
1. 카멜 케이스 언더스코어(memberPoint member_point) 2. .(점) _(언더스코어)
3. 대문자 소문자
// Membmer
package jpabook.jpashop.domain;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@Embedded
private Address address;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
Order에서 ordrItems에 데이터를 넣어두고 저장을 해준다.
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
// persist(orderItemA)
// persist(orderItemB)
// persist(orderItemC)
// 이거만 해주면 위에 3개의 persist는 지워도 된다. persist(order)
그리고 delivery의 값을 setting 해놓으면 order을 저장해 놓으면 delivery entity도 같이 persist 해준다.
@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "delivery_id")
// Cascade을 활용하게 되면 아래처럼 delivery의 값만 세팅해놓고 order만 persist 해주면 delivery의 부분까지 같이 persist가 된다.
private Delivery delivery;
그리고 order와 member가 있으면 member가 주문을 하면 list 안에 order을 넣어줘야 한다.
order에서도 양뱡향이니까 member와 왔다갔다 할 수 있다.
// Member Class
package jpabook.jpashop.domain;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@Embedded
private Address address;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
// Order Class
package jpabook.jpashop.domain;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import static javax.persistence.FetchType.*;
@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id")
private Member member;
Order에서 연관관계 메서드는 아래와 같이 짜주게 된다.(양방향일때 하나의 메서드를 이용해서 묶는 방법이 효율적이다.)
//==연관관계 메서드==//
public void setMember(Member member) {
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);
}
Category에서도 연관관계 편의 메서드를 사용해서 작성한다.
// Category
package jpabook.jpashop.domain.item;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
import static javax.persistence.FetchType.*;
@Entity
@Getter @Setter
public class Category {
@Id
@GeneratedValue
@Column(name = "category_id")
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "category_item",
joinColumns = @JoinColumn(name = "category_id"),
inverseJoinColumns = @JoinColumn(name = "item_id"))
private List<Item> items = new ArrayList<>();
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "parent_id")
private Category parent;
@OneToMany(mappedBy = "parent")
private List<Category> child = new ArrayList<>();
//==연관관계 메서드==//
public void addChildCategory(Category child) {
this.child.add(child);
child.setParent(this);
}
}
엔티티 설계시 주의점을 다시 떠올리면 아래와 같다.
- 엔티티에는 가급적 Setter를 사용하지 말자
-
모든 연관관계는 지연로딩으로 설정!
- 컬렉션은 필드에서 초기화 하자
- 테이블, 컬럼명 생성 전략
<출처 김영한: 실전! 스프링 부트와 JPA 활용1 - 웹 어플리케이션 개발 >
'Spring > SpringBoot' 카테고리의 다른 글
MessageConverter & JPA (0) | 2022.05.06 |
---|---|
스프링부트 개념정리 with JPA - 스프링의 핵심 & 필터 (0) | 2022.05.06 |
도메인 분석 설계 - 엔티티 클래스 개발2 (0) | 2022.04.02 |
도메인 분석 설계 - 도메인 모델과 테이블 설계 (0) | 2022.04.02 |
SpringBoot Error 해결 (0) | 2022.04.02 |