7. 배달플랫폼 - StoreMenu, 연관관계 설정

ys·2024년 3월 5일

배달플랫폼

목록 보기
8/8
  • 이제 store 개발 및, 기능도 간략하게 추가를 했다
  • 이제 해당 가게를 들어가면, 가게의 매뉴를 볼 수 있는 기능을 구현해보자
  • 이 storeMenu는 당연히 store와 연관관계를 갖게 된다
  • 먼저 my_sql에서 해당 테이블을 만들어 보자
  • 하나의 가게는 여러개의 매뉴를 가질 수 있기 때문에 기본적으로 일대다 관계일 것이다
  • 그리고 store_menu에 store_id를 pk로 가지게 될 것이다
  • 이를 가지고 StoreMenuEntity를 만들어보자

StoreMenuEntity

@Getter
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Entity
@Table(name = "store_menu")
public class StoreMenuEntity extends BaseEntity {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "store_id")
    StoreEntity storeEntity;

    @Column(length = 100, nullable = false)
    private String name;

    @Column(precision = 11,scale = 4, nullable = false)
    private BigDecimal amount;

    @Column(length = 50, nullable = false)
    @Enumerated(EnumType.STRING)
    private StoreMenuStatus status;

    @Column(length = 200, nullable = false)
    private String thumbnailUrl;
    private int likeCount;
    private int sequence;
  • 먼저 단방향 관계로 설계한 후 필요하다면 양방향 관계를 설정하자라는 기준을 잡고
  • 단방향 관계로 먼저 설정하였다
  • 일대다 관계에서, 다쪽, 즉 Fk를 관리하는 쪽이 연관관계의 주인이다
  • 먼저 Lazy지연로딩을 해주었고, @JoinColumnpk키store_id를 넣어주었다
  • 그리고 이름, 가격, 상태, url, 좋아요 개수, 시퀀스등을 필드값으로 넣었다

StoreMenuRepository

public interface StoreMenuRepository extends JpaRepository<StoreMenuEntity,Long> {

    // 유효한 매뉴 체크
    // select * from store_menu where id =? and status =? order by id desc limit 1;
    Optional<StoreMenuEntity> findFirstByIdAndStatusOrderByIdDesc(Long id, StoreMenuStatus status);

    // 특정 가게의 매뉴 가져오기
    // select * from store_menu where store_id =? and status =? order by sequence desc;
    List<StoreMenuEntity> findAllByIdAndStatusOrderBySequenceDesc(Long id, StoreMenuStatus status);
}
  • id를 통해 해당 가게를 찾고, 등록된 매뉴중에 하나를 가져오는 findFirstByIdAndStatusOrderByIdDesc
  • id를 통해 해당 가게를 찾고, 등록된 매뉴를 List로 모두 가져오는 findAllByIdAndStatusOrderBySequenceDesc가 있다

  • 이제 Api 모듈로 넘어가서 business, controller, converter, service영역을 알아보자

StoreMenuService

@Service
@RequiredArgsConstructor
public class StoreMenuService {

    private final StoreMenuRepository storeMenuRepository;

    public StoreMenuEntity getStoreMenuWithThrow(Long id){
        Optional<StoreMenuEntity> entity = storeMenuRepository.findFirstByIdAndStatusOrderByIdDesc(id, StoreMenuStatus.REGISTERED);
        return entity.orElseThrow(()-> new ApiException(ErrorCode.NULL_POINT));
    }

    public List<StoreMenuEntity> getStoreMenuStoreId(Long storeId){
        return storeMenuRepository.findAllByIdAndStatusOrderBySequenceDesc(storeId,StoreMenuStatus.REGISTERED);
    }

    public StoreMenuEntity register(StoreMenuEntity storeMenuEntity){
        return Optional.ofNullable(storeMenuEntity)
                .map((it)->{
                    it.changeStatus(StoreMenuStatus.REGISTERED);
                    return storeMenuRepository.save(it);
                }).orElseThrow(()-> new ApiException(ErrorCode.NULL_POINT));
    }

}
  • 가게 id와, 등록된 매뉴중 첫번째 음식을 주는 getStoreMenuWithThrow
  • 가게 id와 등록 매뉴 전부를 리스트로 주는 getStoreMenuStoreId
  • 그리고 StoreMenuEntity를 입력하면 상태를 등록으로 바꾸어주고, 리파지토리를 통해 db에 저장해주는 register 메서드가 있다
  • setter을 지양하기 위해서 changeStatus라는 메서드를 StoreMenuRepository에 추가해준다
public void changeStatus(StoreMenuStatus status){
        this.status = status;
    }

StoreMenuRegisterRequest

@Data
@NoArgsConstructor
@AllArgsConstructor
public class StoreMenuRegisterRequest {
    @NotNull
    private Long storeId;

    @NotBlank
    private String name;

    @NotNull
    private BigDecimal amount;

    @NotBlank
    private String thumbnailUrl;

}
  • 등록을 위해 입력 받는 데이터 형식을 -> Dto로 따로 만들어주었다

StoreMenuResponse

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class StoreMenuResponse {

    private Long id;
    private Long storeId;
    private String name;
    private BigDecimal amount;
    private StoreMenuStatus status;
    private String thumbnailUrl;
    private int likeCount;
    private int sequence;
}
  • 다음은 응답을 위한 데이터를 전달하는 dto 형식이다

StoreConverter

@Converter
public class StoreMenuConverter {

    public StoreMenuEntity toEntity(StoreMenuRegisterRequest request, StoreEntity storeEntity){
        return Optional.ofNullable(request)
                .map(it->{
                    return StoreMenuEntity.builder()
                            .storeEntity(storeEntity)
                            .name(request.getName())
                            .amount(request.getAmount())
                            .thumbnailUrl(request.getThumbnailUrl())
                            .build();
                })
                .orElseThrow(()-> new ApiException(ErrorCode.NULL_POINT));
    }

    public StoreMenuResponse toResponse(StoreMenuEntity storeMenuEntity){
        return Optional.ofNullable(storeMenuEntity)
                .map(it->{
                    return StoreMenuResponse.builder()
                            .id(storeMenuEntity.getId())
                            .name(storeMenuEntity.getName())
                            .storeId(storeMenuEntity.getStoreEntity().getId())
                            .status(storeMenuEntity.getStatus())
                            .amount(storeMenuEntity.getAmount())
                            .thumbnailUrl(storeMenuEntity.getThumbnailUrl())
                            .likeCount(storeMenuEntity.getLikeCount())
                            .sequence(storeMenuEntity.getSequence())
                            .build();
                })
                .orElseThrow(()-> new ApiException(ErrorCode.NULL_POINT));
    }
}
  • 처음에 builder패턴을 이용하여서,
  • 다음과 같이 코딩을 할려고 했지만,,, -> builder패턴이기 때문에, storeEntity즉 연관관계가 된 값을 채워줘야 하는 문제가 생겼다
  • 그래서 어떻게 해야될지 많이 🤔 고민을 했었다...
  • 이게 service 계층을 지금, 역할에 맞게 business, converter, service 3계층으로 나눠서 고민해야할게 생겼던거 같다

✅고민 해결 방법!!!

  • request에는 StoreMenuEntity의 정보가 없다. 오직 store의 id만 있다
  • 결국 business 영역에서 converter을 사용한다
  • business영역에서 storeService를 의존관계 주입을 받아, request의 id를 이용해
  • entity를 넣어서 builder로 생성하자!!
  • 그렇기 위해서는 파라미터로 storeEntity를 받게 코딩을 하였다

StoreMenuBusiness

@Business
@RequiredArgsConstructor
public class StoreMenuBusiness {

    public final StoreService storeService;

    private final StoreMenuService storeMenuService;
    private final StoreMenuConverter storeMenuConverter;

    // register
    @Transactional
    public StoreMenuResponse register(StoreMenuRegisterRequest request){

        StoreEntity store = storeService.findById(request.getStoreId())
                .orElseThrow(()-> new ApiException(ErrorCode.NULL_POINT));
        // req -> entity ->save -> response
        StoreMenuEntity entity = storeMenuConverter.toEntity(request,store);
        StoreMenuEntity newEntity = storeMenuService.register(entity);
        StoreMenuResponse response = storeMenuConverter.toResponse(newEntity);
        return response;
    }

    // 특정 가게 검색

    public List<StoreMenuResponse> search(Long storeId){
        List<StoreMenuEntity> list = storeMenuService.getStoreMenuStoreId(storeId);

        return list.stream()
                .map(storeMenuEntity -> {
                    return storeMenuConverter.toResponse(storeMenuEntity);
                })
                .collect(Collectors.toList());
    }

}
  • storeService까지 di를 받는다
  • 여러 의존관계를 이용해, 비지니스 로직을 만들기 위해서 Business계층을 만들었고
  • 엔티티간 의존관계가 복잡해질 수록 비지니스 계층의 의존관계는 많아질 것이다
  • 아까 말했든 register을 보면, findById를 통해 StoreEntity를 찾고
  • Covnerter.toEntity에서 StoreEntity를 넣어서 builder 패턴을 이용해, StoreMenuRegisterRequest -> StoreMenuEntity로 잘 변환한 것을 알 수 있다
  • 이제 서비스에서 등록을 하고 다시 response로 변환한 후에, 잘 응답으로 전달하였다
  • search또한 리스트로 StoreMenuEntity를 반환받고, 이를 stream을 이용해서, List<StoreMenuResponse>로 잘 반환해서, 반환하였다

StoreMenuOpenApiController

@RestController
@RequiredArgsConstructor
@RequestMapping("/open-api/store-menu")
public class StoreMenuOpenApiController {

    private final StoreMenuBusiness storeMenuBusiness;

    @PostMapping("/register")
    public Api<StoreMenuResponse> register(@Valid @RequestBody Api<StoreMenuRegisterRequest> request){
        StoreMenuRegisterRequest req = request.getBody();
        StoreMenuResponse response = storeMenuBusiness.register(req);
        return Api.OK(response);
    }
}
  • 로그인 필요없이 가게 매뉴를 등록하는 것이다
  • Api 요청을 잘 받은 후, 검증을 하고 business로직에서 등록을 한 후,
  • Api 로직으로 감싸서 보내준다

StoreMenuApiController

@RestController
@RequestMapping("/api/store-menu")
@RequiredArgsConstructor
public class StoreMenuApiController {

    private final StoreMenuBusiness storeMenuBusiness;
    @GetMapping("/search")
    public Api<List<StoreMenuResponse>> search(@RequestParam("storeId") Long storeId){
        List<StoreMenuResponse> response = storeMenuBusiness.search(storeId);
        return Api.OK(response);
    }

}
  • 찾는 것도 가게 id를 받고
  • 가게 아이디를 이용해, 특정 가게의 모든 매뉴를 가져온다

실행

  • 아까만든 별다방 id=4에 자몽에이드라는 매뉴를 5000원에 추가하였다
  • 성공적으로 추가되었고
  • id=4즉 별다방의 매뉴를 검색해 보았고
  • 자몽에이드가 잘 추가되서 나왔다
  • 아이스 아메리카노도 추가한 후, 2개 데이터가 잘 나오는지 확인해 보겠다

어라??? 계속 자몽애이드만 나왔다

🤔 오류 수정

  • 생각을 해보니까 지금 StoreMenuEntiy와 StoreEntity의 pk이 값이 모두 baseEntity를 상속받아서,,,
  • 🤔 StoreEntity가 아닌 StoreMenuEntity의 id를 찾아서, 리스트를 찾는게 아닌,
    자몽애이드만 나오는 것 같다...
  • 즉 리파지토리의 ✅쿼리 id부분을 좀더 명확하게 알려줘야 한다
  • 아직, QueryDsl을 잘 사용하지 못하여서.. @Query 어노테이션을 이용해서 상사하게 알려주었다
@Query("SELECT sm FROM StoreMenuEntity sm " +
            "WHERE sm.storeEntity.id = :storeId " +
            "AND sm.status = :status " +
            "ORDER BY sm.sequence DESC")
    List<StoreMenuEntity> findAllByStoreIdAndStatusOrderBySequenceDesc(@Param("storeId") Long storeId, @Param("status") StoreMenuStatus status);
  • 다음과 같이 자몽에이드와 아이스 아메리카노 두개의 매뉴가 모두 잘 추가 된 것을 알 수 있다!
profile
개발 공부,정리

0개의 댓글