본문 바로가기
Framework and Tool/JPA

JPA - 다양한 연관관계 - 요구사항 분석과 맵핑 2

by ocwokocw 2021. 7. 10.

- 참조: 자바 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]

 

댓글