1. 엔티티 개발
1) Member
@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" ) // 일대다 관계. 연관관계 주인을 넘겨주기 위해 'mappedBy' 활용
private List<Order> orders = new ArrayList<>();
}
id와 name은 일반적인 약속 대로 적었다. (이하 엔티티들은 생략)
- Address는 임베디드 엔티티이므로 @Embedded 어노테이션을 달았다.
- Order는 일대다 관계이므로 지네릭 List로 받고, 연관관계의 주인이 order 쪽에 있으므로 mappedBy를 달아준다.
2) Order
@Entity
@Getter @Setter
@Table( name = "orders" ) // DB에서 'ORDER'를 못 받음
public class Order {
@Id @GeneratedValue
@Column( name = "order_id" )
private Long id;
@ManyToOne( fetch = FetchType.LAZY ) // 다대일
@JoinColumn( name = "member_id" ) // FK 설정
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" ) // order에서 delivery를 찾을 경우가 많으므로 연관관계의 주인으로
private Delivery delivery;
private LocalDateTime orderDate; // 주문 시간
@Enumerated( EnumType.STRING )
private OrderStatus status; // 주문 상태 [ORDER, CANCEL]
}
- @Table로 이름을 명시하는 이유는 DB에서 'ORDER'를 예약어로 걸어뒀기 때문이다.
- Member와는 다대일 관계이고 연관관계의 주인이므로 LAZY fetch와 @JoinColumn으로 FK를 명시해준다.
- OrderItem과는 일대다 관계이므로 mappedBy와 ALL cascade를 걸어준다.
- Delivery와는 일대일 관계이지만, 연관관계의 주인을 Order 쪽에서 가져가기로 했으므로 @JoinColumn으로 FK를 명시해준다.
- orderDate는 주문 시간을 나타내고 LocalDateType을 활용한다.
- OrderStatus는 열거형인데, 여기서 참고해야 할 점이 하나 있다.
- 열거형은 DB에 기본적으로 숫자로 들어간다. ( EnumType.ORDINAL )
- 근데 숫자는 만약 1, 2, 3이 미리 있는 와중에 두번째에 새로운 열거형을 추가하게 되면 곤란해진다.
- 그래서 가급적 STRING으로 명시하는 것이 추천된다.
3) OrderStatus (enum)
public enum OrderStatus {
ORDER,
CANCEL
}
- 주문 상태는 주문과 주문 취소로만 이뤄져 있다.
- 테이블로 만들어지는 것이 아니고 컬럼으로 쓰이기만 하므로 @Entity를 쓰지 않는다.
4) OrderItem
@Entity
@Table( name = "order_item" )
@Getter @Setter
public class OrderItem {
@Id @GeneratedValue
@Column( name = "order_item_id" )
private Long id;
@ManyToOne( fetch = FetchType.LAZY )
@JoinColumn( name = "item_id" )
private Item item;
@ManyToOne( fetch = FetchType.LAZY )
@JoinColumn( name = "order_id" )
private Order order;
private int orderPrice; // 주문 가격
private int count; // 주문 수량
}
- 아마 Order에서와 동일한 이유로 @Table을 쓴 것 같다.
- Item과 다대일 관계이므로 LAZY fetch와 @JoinColumn을 쓴다.
- Order와 다대일 관계이므로 LAZY fetch와 @JoinColumn을 쓴다.
5) Item
@Entity
@Inheritance( strategy = InheritanceType.SINGLE_TABLE )
@DiscriminatorColumn( name = "dtype" )
@Getter @Setter
public abstract 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<Category>();
}
- Item은 상속관계 매핑으로 Album, Book, Movie 총 세 개의 엔티티를 자식으로 갖는다.
- 그래서 @Inheritance를 쓰고, SINGLE_TABLE 전략은 이 세 개 엔티티의 어트리뷰트를 나누지 않고 Item에서 안고 간다는 뜻이다.
- @DiscriminationColumn은 자식 엔티티를 나누는 컬럼의 이름을 'dtype'으로 명시한다.
- 참고로, 비즈니스 로직 상 상품에서 주문상품쪽으로 조회를 갈 일이 없기 때문에 아예 필드로 적지 않았다.
- Category와 다대다 관계라 @ManyToMany를 사용했고, 카테고리쪽에서 상품을 포함하고 제외하는 경우가 많을 것이므로 mappedBy를 걸었다.
6) Album ( Item의 자식 엔티티 )
@Entity
@DiscriminatorValue("A")
@Getter @Setter
public class Album extends Item {
private String artist;
private String etc;
}
- @DiscrimiatorValue로 Item 테이블에서 어떤 이름으로 분류될 것인지 명시한다.
- Movie나 Book도 위와 비슷하게 작성하므로 생략한다.
7) Delivery
@Entity
@Getter @Setter
public class Delivery {
@Id @GeneratedValue
@Column( name = "delivery_id" )
private Long id;
@OneToOne( mappedBy = "delivery", fetch = FetchType.LAZY )
private Order order;
@Embedded
private Address address;
@Enumerated( EnumType.STRING )
private DeliveryStatus status; // READY, COMP
}
- Order와 일대일 관계고 연관관계의 주인을 넘겨줬으므로 mappedBy를 걸었고 LAZY fetch를 두었다.
- Address를 임베디드 엔티티로 가진다.
- Order에서와 비슷하게 @Enumerated의 STRING 타입으로 명시해서 ORDINAL을 피한다.
8) Category
@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 = FetchType.LAZY )
@JoinColumn( name = "parent_id" )
private Category parent;
@OneToMany( mappedBy = "parent" )
private List<Category> categories = new ArrayList<>();
}
- Item과 다대다 관계다.
- 그런데 Order와 Item 간 관계와 다르게 엔티티를 두지 않고, DB로 넘어가면서 중간 테이블을 만들기로 했다.
- 이를 위해 @JoinTable이 필요하고, joinColumns에는 지금 있는 카테고리의 id를, inverseJoinColumns에는 반대편의 id를 건다.
- 다시 한 번 언급하자면, @JoinTable은 별도의 어트리뷰트를 추가할 수 없기 때문에 실무에서 사용하지 않는다.
- 카테고리는 계층 구조를 나타내줘야 한다.
- child는 다대일의 관계를 가지므로 @ManyToOne으로 parent를 하나 가지고, @JoinColumns로 FK를 가진다.
- parent는 일대다의 관계를 가지므로 @OneToMany로 child를 List로 가지고, mappedBy를 건다.
9) Address
@Embeddable
@Getter
public class Address {
private String city;
private String street;
private String zipcode;
protected Address() { }
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
}
- 참고로, 값 타입은 변경이 불가능하도록 설계해야 한다.
- 그래서 Setter를 달지 않는다.
- 기본 생성자는 필수이므로 달아는 두되, protected로 접근 제어자를 걸면 좋다.
2. 엔티티 설계시 주의점
1) 엔티티에는 가급적 Setter를 사용하지 말자.
- 애플리케이션 변경 시에 엔티티가 어느 Setter 혹은 다른 메서드에서 바뀌는지 전부 추적해야 하는 부담이 생긴다.
2) 모든 연관관계는 지연로딩으로 설정하자.
- 디폴트 값인 즉시로딩은 관련된 데이터를 연속적으로 전부 끌어오기 때문에 매우 비효율적이다.
- 대신 연관된 엔티티는 fetch join 혹은 엔티티 그래프 기능을 활용한다.
- @OneToOne, @ManyToOne 관계는 기본이 즉시로딩이므로 꼭 바꿔줘야 한다.
3) 컬렉션은 필드에서 초기화하자.
- NullPointerException에서 안전해진다.
- 하이버네이트는 엔티티를 persist()할 때 컬렉션을 감싸서 하이버네이트의 내장 컬렉션으로 변경한다.
Member member = new Member();
System.out.println(member.getOrders().getClass());
em.persist(member);
System.out.println(member.getOrders().getClass());
//출력 결과
class java.util.ArrayList
class org.hibernate.collection.internal.PersistentBag
- 그래서 만약 persist() 이후에 member에게 다른 ArrayList 컬렉션을 새롭게 두면 하이버네이트가 관리하지 못하게 된다.
4) 테이블, 컬럼 이름 생성 전략
- 자바에서 써준 필드명과 DB에 등록되는 테이블, 컬럼 이름이 다르다.
- 스프링 부트 신규 설정 ( 엔티티(필드) -> 테이블(컬럼) )
- 카멜 케이스 -> 언더스코어 : ex) memberPoint -> member_point
- .(점) -> _(언더스코어)
- 대문자 -> 소문자
5) Cascade
@OneToMany( mappedBy = "order", cascade = CascadeType.ALL )
private List<OrderItem> orderItems = new ArrayList<>();
- 위 코드와 같이 'cascade = CascadeType.ALL'라고 설정하면,
- persist(order)로만 저장해도 order와 연관된 orderItems가 함께 자동으로 persist()에 호출된다.
6) 연관관계 편의 메서드
// 연관관계 편의 메서드
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);
}
- 양방향 관계에서 한쪽에서 참조하는 쪽의 엔티티를 추가하면 다른 쪽에서도 메서드를 호출할 필요 없도록 한꺼번에 서로 더한다.
- 다만, 이는 꼭 연관관계 주인 쪽에서 가지는 것은 아니고 비즈니스 로직 상 주로 접근하는 쪽에서 하면 된다.
- 그래서 위 코드에서도 Order 쪽에서 addOrderItem()을 한다.
***
해당 게시글은 인프런 김영한 님의 "실전! 스프링 부트와 JPA 활용" 수업을 기반으로 작성되었습니다.
'스프링 (인프런) > 스프링부트' 카테고리의 다른 글
상품 도메인 개발 (0) | 2023.07.06 |
---|---|
애플리케이션 구현 준비 (0) | 2023.07.05 |
도메인 모델과 테이블 설계 (0) | 2023.07.03 |
JPA와 DB 설정, 동작확인 (0) | 2023.07.03 |
프로젝트 환경설정 (0) | 2023.07.03 |