참고
Entity 클래스 개발
Member - Order 연관관계(1:N)
NOTE
•
Order와 Member의 관계는 N:1(다대일)이다.
•
N에 해당하는 Order가 연관관계의 주인으로 채택되며 양방향 연관관계(1에 해당하는 Member도 Order의 정보를 컬렉션으로 갖고 있다)로 설정되어 있다.
•
연관관계의 하인(mappedBy - 지정한 필드)은 수정이 발생해도 값이 변경되지 않는다.
Order - Delivery 연관관계(1:1)
NOTE
•
하나의 주문은 하나의 배송을 한다. 즉 1:1 연관관계로 표현할 수 있다.
•
이 때 연관관계의 주인은 access가 많은 즉, 조회할 일이 많은 곳을 선택해서 주인으로 선택하면 된다. ( 해당 예제에서는 Delivery가 조회가 많을거라 판단해 Order의 Delivery가 연관관계의 주인이된다.)
[ 참고]
•
Delivery Entity는 DeliveryStatus가 ENUM 클래스로 되어있다.
•
해당 내용은 다음과 같으며, EnumType.STRING을 해줘야 순서 보장이 된다!
public enum DeliveryStatus {
READY, COMP
}
Java
복사
◦
EnumType.ORDINAL도 존재하는데 이는 순서에 따라 0,1,2.. 값을 매긴다.
◦
더 편해 보일 수 있지만 데이터가 추가되는순간 순서가 섞이면서 위험해지니 꼭 STRING을 쓰자.
Address(값 타입)
@Embeddable
@Getter
public class Address {
private String city;
private String street;
private String zipcode;
// JPA의 기본 전략인 기본 생성자가 있어야 reflector 나 프록시를 사용할 수 있다. (puliic / protected 가능)
protected Address(){}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
}
//...
@Enumerated(EnumType.STRING)
private DeliveryStatus status;
Java
복사
•
값 타입을 사용하기 위해 @Embeddable을 지정하였고, Delivery에서 Address 컬럼에 @Embedded를 선언하였다.
•
값 타입 사용시 불변의 설계가 올바르기 떄문에 생성자를 통해 생성하고, 수정을 못하게 막도록한다. (단 JPA기본 전략떄문에 기본생성자는 있어야 한다.)
[ 참고]
•
값 타입(Embeded)를 사용하는 이유는 주소라는 값 타입 아래 하위 데이터들을 명확하게 표현하기 위해서이다.
•
배송 Entity(주문정보, 도서, 도로명, 우편번호, 배송상태) → 배송 Entity(주문정보, 주소, 배송상태)
Order, OrderItem, Item (N:N 분리)
NOTE
•
Order와 Item의 관계를 살펴보자
◦
하나의 주문은 여러 상품을 가질 수 있고, 하나의 상품은 여러 주문을 가질 수 있다.
◦
이는 N:N(다대다) 관계이며, 이를 풀기위해 OrderItem을 두어 N:N → 1:N으로 나누어서 처리한다.
•
OrderItem의 입장에서 Order와 Item의 연관관계 주인이 되었다.
◦
Order와 OrderItem은 양방향 연관관계가 되어있지만, OrderItem과 Item은 단방향 관계이기 때문에 Item에 별도의 OrderItem 컬렉션이 없는것을 볼 수 있다.
Category, Item 연관관계(N:N)
NOTE
•
Category와 Item의 관계는 N:N이다.
◦
별도의 CategoryItem을 만들어서 N:N을 풀어서 사용하는 것이 올바른 예시이지만, 해당 경우도 예제에서 다루기 때문에 적용해보자
•
RDBMS에선 컬렉션처럼 N:N관계를 사용할 수 없기 떄문에 @JoinTable와 같이 중간 테이블을 만들어준다.
// Categoty.java
@ManyToMany
@JoinTable(name = "category_item",
joinColumns = @JoinColumn(name = "categoy_id"),
inverseJoinColumns = @JoinColumn(name = "item_id")
)
private List<Item> items = new ArrayList<>();
Java
복사
◦
joinCoumns : 현재 PK로 사용할 컬럼 지정
◦
inverseJoinColumns: 반대편에서 PK로 사용할 컬럼 지정
•
Category구조가 계층 구조기 떄문에 자기의 부모(@ManyToOne), 자기의 자식(@OneToMany) 즉, 자기 자신을 양방향 연관관계로 설정했다.
•
item Entity를 살펴보면 하위 타입들을 지정하여 싱글 테이블 전략을 사용한다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
@Getter
@Setter
public class Item {
@Id
@GeneratedValue
@Column(name = "item_id")
private Long id;
private String name;
private int price;
private int stockQuantity;
@ManyToMany(mappedBy = "items")
private List<Category> categories = new ArrayList<>();
}
Java
복사
Item.java
@Entity
@DiscriminatorValue("M")
@Getter
@Setter
public class Movie extends Item {
private String director;
private String actor;
}
Java
복사
Movie.java
◦
@Inheritance (상속 전략)
▪
디폴트가 싱글테이블, JOINED, TABLE_PER_CLASS 등 존재
◦
@DiscriminatorColumn
▪
하위 클래스를 구분하기 위한 컬럼명 지정 (부모에서 설정)
◦
하위 엔티티들은 Item을 상속 받고 @DiscriminatorValue() 를 지정하여 사용될 데이터를 지정
Entity 설계시 주의점
NOTE
•
Entity는 가급적 Setter를 사용하지 말자
•
모든 연관 관계는 지연 로딩으로 설정
◦
즉시로딩(Eager)은 예측이 어렵고, 어떤 SQL이 실행될지 추적이 어렵다.
◦
JPQL에서 N+1문제 자주 발생
◦
지연로딩(LAZY)으로 꼭 설정해주자.
◦
연관된 Entity를 함께 DB에서 조회해야하면, fetch join 또는 Entity Graph기능
사용
◦
@~ToOne 관계는 기본이 즉시 로딩이므로, 직접 지연로딩을 설정해야한다.
•
컬렉션은 필드에서 초기화 하자.
@OneToMany(mappedBy = "member")
// 선택 Null 문제에서 안전, 하이버네이트 기능 문제 해결
private List<Order> orders = new ArrayList<>();
/*
public Member() {
orders = new ArrayList<>();
}
*/
Java
복사
◦
컬렉션을 초기화 하는 방법은 필드, 생성자 2가지가 존재한다.
◦
아래의 이유로 필드에서 초기화 하는것이 best이다.
Member member = new Member();
System.out.println(member.getOrders().getClass());
em.persist(team); //영속성 컨텍스트에서 관리
System.out.println(member.getOrders().getClass());
/*
- 출력 결과
class java.util.ArrayList
class org.hibernate.collection.internal.PersistentBag
*/
Java
복사
◦
하이버네이트는 컬렉션이 영속성 컨텍스트에 들어오면 추적을 위해 컬렉션을 감싸 내장컬렉션으로 변경한다.
테이블, 컬럼명 생성 전략
NOTE
•
스프링 부트 설정( Entity 필드 → 테이블 컬럼 )
◦
카멜 케이스 → 언더 스코어 ( ex memberPoint → member_point )
◦
. → _
◦
대문자 → 소문자
논리명 생성
•
명시적으로 컬럼, 테이블 명을 적지 않으면 ImplicitNameStrategy 사용
•
spring.jpa.hibernate.naming.implicit-strategy
◦
테이블이나, 컬럼명을 명시하지 않을 때 논리명 적용
물리명 적용
•
spring.jpa.hibernate.naming.physical-strategy
◦
모든 논리명에 적용됨, 실제 테이블에 적용
CascadeType 설정
NOTE
@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;
Java
복사
•
orderItems와 delivery필드에 CascadeType.ALL을 설정
•
해당 기능은 Order를 persist()할 때 연관된 컬렉션들이 같이 영속화 하며, 삭제할 때 같이 삭제하게 됨.
연관관계 편의 메서드
NOTE
/*
Member member = new Member();
Order order = new Order();
member.getOrders().add(order); // 연관관계의 하인인 Member엔티티의 Orders 컬렉션에 order를 넣어주는 기능
order.setMember(member); // 연관관계의 주인인 Order 엔티티의 member 컬렉션에 order를 넣어주는 기능
*/
public void setMember(Member member) { // Order 엔티티 기준
this.member = member; // 현재의 member 컬렉션에 넣어준다.
member.getOrders().add(this); // member에서 Member 엔티티의 Orders를 탐색하여 member를 넣어준다.
}
Java
복사
•
양방향 연관관계를 구현했을 때, 한 곳에 데이터를 생성하면 반대편에도 값을 넣어주어야
한다.
•
해당 부분을 빠뜨리기 쉬운 부분이므로 메서드 안에서 양쪽 모두 넣어주는 기능을 구현해주는 것이 바람직하다.