text/Java

FK-FK table springboot @ID 매핑

hoonzii 2023. 6. 21. 13:53
반응형

토이 프로젝트 만들다가 막혀서 비슷하게 한번 만들어본 상황.

 

요구사항

사용자가 메뉴를 보고 주문을 한다. 메뉴들이 여러 개 있고, 주문 내용에는 메뉴”들” 정보와 요구사항이 포함되어 있을 때

테이블로 위 요구사항을 저장하려면? 뭐… 정답은 없겠지만 내가 구성한 건 아래와 같다.

orderInfo 테이블은 주문정보를 저장하는 테이블이다.

사용자의 요구사항을 저장하는 orderDesc 컬럼과 등록시각을 저장하는 regDate 컬럼이 존재한다.

 

item 테이블은 메뉴 정보를 저장하는 테이블이다.

메뉴 이름을 저장하는 name 컬럼과 가격을 저장하는 price 컬럼, 등록시각을 저장하는 regDate컬럼이 존재한다.

 

orderItem은 주문 정보에 포함되는 메뉴들을 저장하는 테이블이다.

주문 정보는 여러 개 저장될 수 있고,

주문 정보 하나에 여러 메뉴가 포함될 수 있다. (N:M 관계, 다대다 관계)

(fk-fk table 이기에 pk가 없이 구성한다…!)

위 조건으로 table을 실제로 만들어보자. db는 mysql을 이용했다.

create table orderInfo(
    `seq` bigint not null auto_increment,
    `orderDesc` varchar(20) null,
    `regDate` timestamp default current_timestamp,
    primary key(`seq`)
)
engine=InnoDB;

create table item(
    `seq` bigint not null auto_increment,
    `name` varchar(20) not null,
    `price` int not null default 0,
    `regDate` timestamp default current_timestamp,
    primary key(`seq`)
)
engine=InnoDB;

create table orderItem(
    `orderInfo_seq` bigint not null,
    `item_seq` bigint not null,
    foreign key (`orderInfo_seq`) references orderInfo(`seq`),
    foreign key (`item_seq`) references item(`seq`)
)
engine=InnoDB;

item 넣기

INSERT INTO item(name, price) VALUES ('americano' , 4000),('latte' , 4500), ('bagel' , 5000);
SELECT * FROM item WHERE seq > 0;

 

DB에 직접 위 요구사항대로 주문을 넣는다면

주문 시

  1. 주문 정보(orderInfo) 생성
  2. orderInfo pk와 item pk를 orderItem에 저장
# 주문 정보 생성
INSERT INTO orderInfo(orderDesc) VALUES ('아메리카노는 아이스, 주차x');

# 주문 정보(orderInfo)의 pk + 아이템(item)의 pk 로 주문 아이템(orderItem) insert
INSERT INTO orderItem(orderInfo_seq, item_seq) VALUES (1, 1),(1, 2),(1, 3);

 

sql을 작성해 결과를 확인해 본다.

SELECT orderinfo.orderDesc, item.`name`
FROM orderinfo
JOIN orderitem ON orderinfo.seq = orderitem.orderInfo_seq
JOIN item ON orderitem.item_seq = item.seq
WHERE orderinfo.seq = 1;

 

spring initializer를 통해 spring boot 프로젝트를 급하게 만들어준다.

mysql에 맞게 application.properties를 급하게 세팅해 준다.

spring.datasource.url=jdbc:mysql://localhost/localDB?useUnicode=true&characterEncoding=utf8&verifyServerCertificate=false&useSSL=false
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

 

Domain package

OrderInfo 테이블과 Item 테이블의 경우 아래와 같이 Entity 객체를 구성할 수 있다.

// orderInfo - 주문 정보
@Entity
@Table(name="orderinfo")
@DynamicInsert
@Data
public class OrderInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long seq;

    @Column(name="orderDesc")
    private String orderDesc;

    @Column
    private LocalDateTime regDate;
}

// item - 메뉴
@Entity
@Table(name="item")
@DynamicInsert
@Data
public class Item {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long seq;

    private String name;
    private int price;

    @Column
    private LocalDateTime regDate;

}

 

문제발생

이제 내가 문제시 삼은 부분이 나온다.

FK-FK로 연결된 테이블의 경우, id 값이 존재하지 않을 때는 객체 @Id는 어떻게 세팅해야 하지…?

구글링에서 답을 찾았다.

JPA Primary Key

 

JPA Primary Key

JPA Primary Key Every entity object that is stored in the database has a primary key. Once assigned, the primary key cannot be modified. It represents the entity object as long as it exists in the database. As an object database, ObjectDB supports implicit

www.objectdb.com

위와 같은 상황을 composite primary key라고 한다고…

위 글을 참고해 @Id를 구성해 준다.

FK 2개를 담는 id 클래스(OrderItemId.class)를 만들어준다.

import java.io.Serializable;

public class OrderItemId implements Serializable {
    Long orderInfo_seq;
    Long item_seq;
}

주의사항

composite-id의 경우, implements Serializable 하지 않으면 아래와 같은 오류가 난다

Caused by: org.hibernate.MappingException: 
Composite-id class must implement Serializable: 
com.example.EmbeddableDemo.domain.OrderItemId

그리고 OrderItem 클래스 선언 시 @IdClass를 명시해 준다.

이때 @Id 어노테이션은 붙여야 한다. (안 그럼 No identifier specified for entity 발생)

@Entity
@Table(name = "orderitem")
@Data
@IdClass(OrderItemId.class)
public class OrderItem implements Serializable {

    @Id
    private Long orderInfo_seq;

    @Id
    private Long item_seq;

}

 

다시 연관 관계를 보자면 table 간 pk-fk 연결은 이렇게 되어 있으니 이전에 만들어둔

orderInfo 클래스랑 item 클래스를 수정해 준다.

@Entity
@Table(name="orderinfo")
@DynamicInsert
@Data
public class OrderInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long seq;

    @Column(name="orderDesc")
    private String orderDesc;

    @OneToMany(mappedBy = "orderInfo_seq", cascade = CascadeType.ALL)
    private List<OrderItem> orderItem; // -> 추가된 부분

    @Column
    private LocalDateTime regDate;
}

@Entity
@Table(name="item")
@DynamicInsert
@Data
public class Item {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long seq;

    @OneToMany(mappedBy = "item_seq", cascade = CascadeType.ALL)
    private List<OrderItem> orderItem; //-> 추가된 부분

    private String name;
    private int price;

    @Column
    private LocalDateTime regDate;

}

*cascade 부분의 경우, 주문 내용이 사라지면 주문 아이템 역시 사라져야 하기에 Cascade.ALL 추가

(Item 역시 마찬가지…!)

@OneToMany(mappedBy = "orderInfo_seq", cascade = CascadeType.ALL)
private List<OrderItem> orderItem; // -> 추가된 부분

 

RestController를 구성해 간단하게 조회, 추가를 만들어보자.

조회 - getOrder

  • OrderInfo pk 값으로 orderInfo와 orderItem을 조회
@GetMapping("/getOrder")
public Order getOrder(@RequestParam(name = "orderNum") Long orderNum) {
    // 1. order 가져오기
    OrderInfo orderInfo = orderInfoService.getOrderInfo(orderNum);

    // 2. orderItem 가져오기
    List<OrderItem> orderItemList = orderInfo.getOrderItem();

    // 3. item 가져오기 & menu 반환
    List<String> menuItems = new ArrayList<>();
    orderItemList.forEach(orderItem -> {
        Item item = itemService.itemReturn(orderItem.getItem_seq());
        if(item != null)
            menuItems.add(item.getName());
    });
		// 4. OrderDto 로 반환
    Order order = new Order();
    order.setOrderDesc(orderInfo.getOrderDesc());
    order.setMenuList(menuItems);

    return order;
}

//1.order 가져오기 service(OrderService.class) 내 함수 
public OrderInfo getOrderInfo(Long seq) {
    Optional<OrderInfo> optionalOrderInfo = orderInfoRepository.findById(seq);
    return optionalOrderInfo.orElse(null);
}

//3.item 가져오기 service(ItemService.class) 내 함수
public Item itemReturn(Long item_id) {
    return itemRepository.findById(item_id).orElseThrow(null);
}

// 4. Order DTO 클래스
@Data
public class Order {
    private String orderDesc;
    private List<String> menuList;
}

postman으로 get 실행

추가 - insertOrder

  • Order DTO 형식으로 주문 시 주문 정보 저장
@PostMapping("/insertOrder")
public String order(@RequestBody Order order) {
    String orderDesc = order.getOrderDesc();
    List<String> menuList = order.getMenuList();

    // 1. item 반환
    List<Item> itemList = itemService.itemListReturn(menuList);

    // 2. 주문 정보 생성 -> empty orderItems
    OrderInfo orderInfo = new OrderInfo();
    orderInfo.setOrderDesc(orderDesc);
    Long saveOrderInfo = orderInfoService.saveOrderInfo(orderInfo);
    
    // 3. 주문 아이템 리스트 생성
    List<OrderItem> orderItemList = new ArrayList<>();
    OrderInfo serviceOrderInfo = orderInfoService.getOrderInfo(saveOrderInfo);
    itemList.forEach(item -> {
        OrderItem orderItem = new OrderItem();
        orderItem.setOrderInfo_seq(serviceOrderInfo.getSeq());
        orderItem.setItem_seq(item.getSeq());
        orderItemList.add(orderItem);
    });
    
    // 4. 주문 아이템 저장 
    orderItemService.saveOrderItems(orderItemList);
    return saveOrderInfo.toString(); // 저장된 orderInfo 의 pk반환 
}

//1. item 반환 함수 (ItemService.class)
public List<Item> itemListReturn(List<String> menuNames) {
    List<Item> itemList = new ArrayList<>();
    menuNames.forEach(menuName -> {
        Optional<Item> optionalItem = itemRepository.findByName(menuName);
        optionalItem.ifPresent(itemList::add);
    });
    return itemList;
}

//2.주문 정보 생성 함수 -> empty orderItems (OrderInfoService.class)
@Transactional
public Long saveOrderInfo(OrderInfo orderInfo) {
    OrderInfo info = orderInfoRepository.save(orderInfo);
    return info.getSeq();
}

// 4. 주문 아이템 저장 함수
@Transactional
public void saveOrderItems(List<OrderItem> orderItemList) {
    orderItemList.forEach(orderItem -> {
        orderItemRepository.save(orderItem);
    });
}

postman으로 post 실행

실제로도 잘 들어갔는지 DB에서 확인해 보자.

SELECT orderinfo.orderDesc, item.`name`
FROM orderinfo
JOIN orderitem ON orderinfo.seq = orderitem.orderInfo_seq
JOIN item ON orderitem.item_seq = item.seq
WHERE orderinfo.seq = 5;

 

전체 코드는 여기서

 

GitHub - hoonzinope/SprintBootStudy: Sprint boot 강의 듣고 구현체 깃 업로드

Sprint boot 강의 듣고 구현체 깃 업로드. Contribute to hoonzinope/SprintBootStudy development by creating an account on GitHub.

github.com

 

반응형