1.조건
2.Entity 설계
@Entity
@Getter
@Setter
@Table(name = "menu")
@NoArgsConstructor(access = AccessLevel.PROTECTED) // protected로 생성자 만들기
public class Menu {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "menu_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "restaurant_id")
private Restaurant restaurant;
private String foodName;
private int foodPrice;
//==생성 메서드==//
public static Menu createMenu(Restaurant restaurant, String foodName, int foodPrice){
Menu menu = new Menu();
menu.setRestaurant(restaurant);
menu.setFoodPrice(foodPrice);
menu.setFoodName(foodName);
return menu;
}
}
Menu 엔티티는 Restaurant, OrderItem 엔티티와 다대일 관계를 가지고 있지만, OrderItem 엔티티와는 양뱡향을 가지지 않고 단뱡향을 가지도록 설계했다.
양뱡향이란, 자식도 부모를 호출해서 사용할 수 있도록 설계하는 것인데, 비즈니스 로직에서 Menu가 어디 OrderItem에 속해 있을지는 크게 중요하지 않다고 판단했다.
3.MenuRequestDto
@Data
public class MenuRegisterDto {
private Long restaurantId;
private String name;
@Max(value = 1000000)
@Min(value = 100)
private int price;
@AssertTrue(message = "100원 단위로 입력하세요.")
private boolean priceCheck;
public boolean priceBound(){
if(getPrice() % 100 == 0){
return true;
}else{
return false;
}
}
public MenuRegisterDto(Long restaurantId, String name, int price, boolean priceCheck) {
this.restaurantId = restaurantId;
this.name = name;
this.price = price;
this.priceCheck = priceBound();
}
}
설계 조건에 해당하는 부분은 Validation을 사용했다. 서비스 로직에서 Exception에 대해 던져줘도 되지만, 이번 설계는 도메인 로직 구현에 집중했기 때문에 Dto에서 값을 받아올 때부터 처리하도록 설계했다.
4.MenuRepository
@Repository
@RequiredArgsConstructor
public class MenuRepository {
private final EntityManager em;
//메뉴 저장
public void save(Menu menu){
em.persist(menu);
}
public List<Menu> findAll(Long restaurantId){
return em.createQuery("select m from Menu m where m.restaurant.id = :restaurantId", Menu.class)
.setParameter("restaurantId", restaurantId)
.getResultList();
}
public Menu findOne(Long menuId){
return em.find(Menu.class, menuId);
}
public Menu findByName(String menuName, Long restaurantId){
try {
return em.createQuery(" select m from Menu m where m.foodName = :menuName and m.restaurant.id = :restaurantId",Menu.class)
.setParameter("menuName", menuName)
.setParameter("restaurantId", restaurantId)
.getSingleResult();
}catch (NoResultException e){
return null;
}
}
}
다른 메서드는 단순 저장 & 조회 로직이라서 크게 알아볼 것은 없다.
마지막 메서드인 findByName에서는 설계 조건 중 하나인 하나의 식당에서 중복된 이름의 메뉴를 등록시킬 수 없는 제약사항을 구현했다.
menuName과 restaurantId를 입력받아 restaurantId의 메뉴에서 menuName과 같은게 있는지 확인했고, NoResultException에 대해서 처리해줬다.
5.MenuService
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MenuService {
private final MenuRepository menuRepository;
private final RestaurantRepository restaurantRepository;
//메뉴 저장
@Transactional
public void save(List<MenuRegisterDto> menuRegisters, Long restaurantId) {
Restaurant restaurant = restaurantRepository.findOne(restaurantId);
for(MenuRegisterDto menuRegister:menuRegisters){
Menu menu = Menu.createMenu(restaurant,menuRegister.getName(), menuRegister.getPrice());
Menu byName = menuRepository.findByName(menu.getFoodName(), restaurantId);
if (byName == null){
menuRepository.save(menu);
}else {
throw new IllegalArgumentException("해당 메뉴가 존재합니다.");
}
}
}
//메뉴 조회
public List<Menu> findAll(Long restaurantId){
return menuRepository.findAll(restaurantId);
}
}
서비스에선 메뉴를 저장할 때 앞서 보았던 menuRepository.findByName을 먼저 실행시킨 후 결과가 null일 경우(NoResultException 이 발생할 경우)에만 save되도록 설계했다. 만약 null이 아닐경우 Exception을 통해 트랜잭션 롤백되도록 설계했다.
6.MenuController
@Validated
@RestController
@RequiredArgsConstructor
public class MenuController {
private final MenuService menuService;
@PostMapping("/restaurant/{restaurantId}/food/register")
public void save(@RequestBody @Valid List<MenuRegisterDto> menuRegisters, @PathVariable Long restaurantId){
menuService.save(menuRegisters, restaurantId);
}
@GetMapping("/restaurant/{restaurantId}/foods")
public JsonResult findAll(@PathVariable Long restaurantId){
List<Menu> menus = menuService.findAll(restaurantId);
List<MenuResponse> collect = menus.stream()
.map(m -> new MenuResponse(m.getId(), m.getFoodName(), m.getFoodPrice()))
.collect(Collectors.toList());
return new JsonResult(collect);
}
}
메뉴 컨트롤러에서는 stream과 map을 활용해 응답받은 Menu 엔티티 객체를 MenuResponse 객체로 변경해서 결과에 담아 주었다.
7.JsonResult
@Data
@AllArgsConstructor
public class JsonResult<T> {
private T data;
}
이번에 새롭게 알게된 사실인데, Dto 객체를 바로 넘겨주기 보단 다른 클래스로 감싸서 보내게 된다면 json의 데이터별로 이름을 씌어서 보내줄 수가 있다고 한다.
잘 써먹어야지.