- 참조: 자바 ORM 표준 JPA 프로그래밍
- 실전 예제
이전 글 JPA - 엔티티 맵핑 - 요구사항 분석과 맵핑 에 이어서 요구사항 분석을 통해 1 : 1 및 M : N 연관관계 맵핑을 적용해보자.
추가된 요구사항은 아래와 같다.
- 상품을 주문할 때 배송정보를 입력할 수 있다. 하나의 주문에서는 하나의 배송지로만 배송할 수 있다.
- 상품을 카테고리로 구분할 수 있다.
아래 다이어그램은 위의 요구사항을 반영하여 UML 을 다시 그린것이다.
이전 UML 과 비교해서 Delivery 와 Category 클래스가 추가되었다.
먼저 Order 와 Delivery 를 살펴보자. 하나의 주문시 하나의 배송지에만 배송할 수 있으므로 둘은 1 대 1 관계이다. 또 주문에서만 배송지 정보를 참조하는게 아니라 배송지 정보에서도 주문을 참조할 수 있어서 양방향 관계이다.
다음으로 Category 와 Item 을 살펴보자. 하나의 카테고리 하위에는 여러 개의 상품이 있다. 또 하나의 아이템은 2 개 이상의 카테고리에 포함될 수 있으므로 둘은 M : N 관계이다.
카테고리는 제품과의 관계외에도 생각할거리가 있는데 상위 카테고리와 하위 카테고리를 가질 수 있다. 그래서 parent 와 child 필드를 추가하였다. 1 개의 부모 카테고리만 가질 수 있다고 가정하면 parent 의 다중성은 1 이며, 하위 카테고리는 여러 개 일 수 있으므로 다중성을 * 로 표시하였다.
- Java example
1 : 1 관계인 Order 와 Delivery 엔티티부터 코드를 작성해보자.
@Entity
@Table(name = "ORDERS")
public class Order {
@Id
@GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems = new ArrayList<>();
@Temporal(TemporalType.TIMESTAMP)
private Date orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@OneToOne
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery;
Order 와 Delivery 는 1 : 1 양방향 관계 이므로 둘다 @OneToOne 다중성을 사용하면 된다. 다만 연관관계의 주인을 정해야 하는데, 주문정보에서 배송정보를 참조하는 경우가 많으므로 주문에서 외래키를 관리하게 한다.
public enum DeliveryStatus {
READY, SHIPPING, COMPLETED
}
.........
@Entity
public class Delivery {
@Id @GeneratedValue
@Column(name = "DELIVERY_ID")
private Long id;
private String city;
private String street;
private String zipcode;
@Enumerated(EnumType.STRING)
private DeliveryStatus status;
@OneToOne(mappedBy = "delivery")
private Order order;
Delivery 는 위와 같이 작성한다. 앞에서 언급했듯이 연관관계의 주인은 주문이므로 mappedBy 속성을 이용한다.
다음으로 Category 와 Item 을 살펴보자. Category 와 Item 은 M : N 관계이다. Item 은 여러 개의 카테고리를 지정할 수 있으므로 @ManyToMany 다중성을 이용한다. 외래키는 카테고리에서 관리해서 mappedBy 로 Category 에서 Item 을 참조하는 필드명 items 를 설정하였다.
@Entity
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<>();
Category 는 다소 복잡하다. 일단 Item 과의 관계먼저 살펴보자.
Category 와 Item 은 M : N 이므로 @ManyToMany 를 사용하였다. DB 상으로는 1 : M, N : 1 관계를 풀어내는 조인테이블을 사용하지만 Item 과 Category 의 외래키 이외의 속성들을 관리하지 않으므로 연결 엔티티를 두지 않고 @JoinTable 만 사용했다.
다음으로 자신의 상위 카테고리를 나타내는 parent 필드가 있다. 자기자신 : 상위 카테고리는 N : 1 이므로 @ManyToOne 다중성을 이용한다. 외래키는 다중성 N 에서 관리하므로 @JoinColumn 으로 외래키를 지정한다. 이때 Tree 구조를 표현할 수 있도록 CATEGORY_ID 가 아닌 별도의 외래키 PARENT_ID 를 지정해야 한다. 자기자신의 PK 를 외래키로 지정하면 무한루프에 빠지게 된다.
자기자신 : 하위 카테고리는 1 : N 이므로 @OneToMany 다중성을 사용한다. 외래키는 N 쪽에서 관리하므로 mappedBy 속성을 이용한다. 여기서 mappedBy 를 어떻게 지정해야할까 헷갈릴 수 있다. 이럴때에는 가상의 엔티티를 생각해본다. 자기자신을 나타내는 Category 와 하위 카테고리를 나타내는 가상의 ChildCategory 가 있다고 가정해보자. ChildCategory 에서는 Category 를 parent 로 참조할 것이다. 그러므로 mappedBy 에는 "parent" 가 와야 한다.
@Entity
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
@JoinColumn(name = "PARENT_ID")
private Category parent;
@OneToMany(mappedBy = "parent")
private List<Category> child = new ArrayList<>();
회원이 인형을 주문하는 시나리오를 생각해보자. 순서는 아래와 같다.
- '인형' 카테고리 생성, 인형 하위에 '포켓몬스터' 카테고리 생성
- '잠맘보' 인형을 '인형' 카테고리와 '포켓몬스터' 카테고리 아이템에 종속
- USER#1 회원가입
- 주문생성하여 USER#1 의 주소를 기반으로 배송지정보 입력
- '잠맘보' 인형 2 개를 주문
아래 코드를 보면서 위의 코드를 천천히 따라가보길 바란다.
public static void save(EntityManager em) {
EntityTransaction tx = em.getTransaction();
tx.begin();
//인형 카테고리 생성
//인형 카테고리 하위에 포켓몬스터 카테고리 생성
//잠맘보 카테고리에 종속
Category doll = new Category();
doll.setName("인형");
doll.setParent(null);
em.persist(doll);
Category pocketMonster = new Category();
pocketMonster.setName("포켓몬스터");
pocketMonster.setParent(doll);
pocketMonster.setChild(null);
em.persist(pocketMonster);
Item jammambo = new Item();
jammambo.setName("JAMMAMBO#1");
jammambo.setPrice(35000);
jammambo.setStockQuantity(100);
em.persist(jammambo);
doll.getItems().add(jammambo);
pocketMonster.getItems().add(jammambo);
//User 회원가입
Member member = new Member();
member.setName("USER#1");
member.setCity("CITY#1");
member.setStreet("STREET#1");
member.setZipcode("ZIPCODE#1");
em.persist(member);
Delivery userDelivery = member.getUserDelivery();
userDelivery.setStatus(DeliveryStatus.READY);
em.persist(userDelivery);
//주문생성, 회원의 배송지 입력
Order order = new Order();
order.setOrderDate(new Date());
order.setDelivery(userDelivery);
order.setStatus(OrderStatus.ORDER);
order.setMember(member);
em.persist(order);
//잠맘보 인형 2개 장바구니 추가
Item jammamboItem = em.find(Item.class, jammambo.getId());
OrderItem orderItem = new OrderItem();
orderItem.setOrder(order);
orderItem.setCount(2);
orderItem.setItem(jammamboItem);
orderItem.setOrderPrices(jammamboItem.getPrice());
em.persist(orderItem);
tx.commit();
em.close();
}
아래 코드는 USER#1 이 자신의 주문정보를 조회한다고 가정하고 테스트코드를 작성한것이다.
public static void find(EntityManager em) {
EntityTransaction tx = em.getTransaction();
tx.begin();
Optional<Member> resultMember =
em.createQuery("select m from Member m where m.name=:username", Member.class)
.setParameter("username", "USER#1")
.getResultStream()
.findAny();
if(!resultMember.isPresent()) {
System.out.println("There is no one.");
return;
}
Member member = resultMember.get();
System.out.println("====User orders====");
List<Order> orders = member.getOrders();
orders.forEach(System.out::println);
System.out.println("====Order items====");
orders.forEach(order -> {
order.getOrderItems().forEach(orderItem -> {
System.out.println("orderItem: " + orderItem);
System.out.println("Item : " + orderItem.getItem());
});
});
tx.commit();
em.close();
}
Console 의 로그 결과는 아래와 같다.
====User orders====
Hibernate:
select
orders0_.member_id as member_i5_6_0_,
orders0_.order_id as order_id1_6_0_,
orders0_.order_id as order_id1_6_1_,
orders0_.delivery_id as delivery4_6_1_,
orders0_.member_id as member_i5_6_1_,
orders0_.order_date as order_da2_6_1_,
orders0_.status as status3_6_1_,
delivery1_.delivery_id as delivery1_2_2_,
delivery1_.city as city2_2_2_,
delivery1_.status as status3_2_2_,
delivery1_.street as street4_2_2_,
delivery1_.zipcode as zipcode5_2_2_
from
orders orders0_
left outer join
delivery delivery1_
on orders0_.delivery_id=delivery1_.delivery_id
where
orders0_.member_id=?
Hibernate:
select
orderitems0_.order_id as order_id5_5_0_,
orderitems0_.order_item_id as order_it1_5_0_,
orderitems0_.order_item_id as order_it1_5_1_,
orderitems0_.count as count2_5_1_,
orderitems0_.item_id as item_id4_5_1_,
orderitems0_.order_id as order_id5_5_1_,
orderitems0_.order_prices as order_pr3_5_1_,
item1_.item_id as item_id1_3_2_,
item1_.name as name2_3_2_,
item1_.price as price3_3_2_,
item1_.stock_quantity as stock_qu4_3_2_
from
order_item orderitems0_
left outer join
item item1_
on orderitems0_.item_id=item1_.item_id
where
orderitems0_.order_id=?
Order [id=6, member=com.example.demo.member.Member@248d3a, orderItems=[com.example.demo.member.OrderItem@76ffc17c], orderDate=2021-07-10 12:09:36.816, status=ORDER, delivery=Delivery [id=5, city=CITY#1, street=STREET#1, zipcode=ZIPCODE#1, status=READY]]
====Order items====
orderItem: com.example.demo.member.OrderItem@76ffc17c
Item : Item [id=3, name=JAMMAMBO#1, price=35000, stockQuantity=100]
'Framework and Tool > JPA' 카테고리의 다른 글
JPA - 고급맵핑 - MappedSuperclass (0) | 2021.07.10 |
---|---|
JPA - 고급맵핑 - 슈퍼타입과 서브타입 (0) | 2021.07.10 |
JPA - 다양한 연관관계 - M : N 비식별관계 (0) | 2021.07.10 |
JPA - 다양한 연관관계 - M : N 식별관계 (0) | 2021.07.10 |
JPA - 다양한 연관관계 - M : N (0) | 2021.07.08 |
댓글