세션 변경, JPA Optional, insert if not exist

JungWooLee·2022년 8월 29일
0

SpringBoot ToyProject

목록 보기
11/14

이어서 하기

  • 세션 정보를 통하여 x,y 값을 받고 근처 맛집중 db 에 존재하지 않는 레스토랑의 경우 insert를 진행합니다
  • 레스토랑 view

1. insert if not exist 수행하기

1. Optional<Restaurant> 을 통하여 findById 테스트

Repository

public interface RestaurantsRepository extends JpaRepository<Restaurant, Long> {
    @Query(value = "select r.id from Restaurant r", nativeQuery = true)
    public List<Long> findAllreturnId();

    @Query(value = "SELECT R.ID, " +
            "R.BUSINESS_HOURS, R.BOOKING_REVIEW_SCORE," +
            "R.NAME,R.IMAGE_URL,R.CATEGORY," +
            "R.RESTAURANT_TYPE,R.SAVE_COUNT," +
            "R.VISITOR_REVIEW_SCORE," +
            "ST_Distance_Sphere(Point(:x,:y),POINT(R.X, R.Y)) AS diff_Distance," +
            "R.X," +
            "R.Y," +
            "R.ADDRESS " +
            "FROM RESTAURANT AS R HAVING diff_Distance <= 1000 order by diff_Distance", nativeQuery = true)
    public List<RestaurantDTOInterface> getRestaurantByLocation(@Param("x") Double x, @Param("y") Double y);

    Optional<Restaurant> findRestaurantById(long id);
}

Optional 사용 이유 ?

각 레스토랑은 유일한 ID 값을 가지는 것을 확인하였습니다. Optional 동작시 반환값이 null 일 경우에 respository.findRestaurantById.orElse() 를 통하여 값이 없는 경우 repository에 추가할 수 있도록 합니다

Repository 테스트

	@Test
    @DisplayName("카테고리별 음식점 모두 스크래핑후 보내기")
    @BeforeEach
    public void getRestaurantData_v2 () throws Exception {

        for (RestaurantType type : RestaurantType.values()) {
            // 카테고리내의 모든 음식들을 크롤링
            String url = "/graphql";
            String _url = HOST_v2+url;
            GetRestaurantRequest request = GetRestaurantRequest.builder()
                    .x(x)
                    .y(y)
                    .bounds("126.9738873;37.5502692;126.9980272;37.5696434")
                    .query("음식점")
                    .type(type)
                    .build();
            String jsonOperation = naverUtility.getRestaurants(request);
            HttpHeaders httpHeaders = utility.getDefaultHeader();

            HttpEntity requestMessage = new HttpEntity(jsonOperation,httpHeaders);
            ResponseEntity response = restTemplate.exchange(
                    _url,
                    HttpMethod.POST,
                    requestMessage,
                    String.class);
            List<Restaurant> entities = new ArrayList<>();

            JSONArray datas = new JSONArray(response.getBody().toString());
            datas.getJSONObject(0);

            JSONArray items = datas.getJSONObject(0).getJSONObject("data").getJSONObject("restaurants").getJSONArray("items");
            int total = Integer.parseInt(datas.getJSONObject(0).getJSONObject("data").getJSONObject("restaurants").get("total").toString());
            int maxCnt = total < 100 ? total : 100;
            for (int i = 0; i < maxCnt; i++) {
                GetRestaurantResponse mapped_data = gson.fromJson(items.get(i).toString(), GetRestaurantResponse.class);
                //1. first map with entity : 엔티티와 매핑하기전 validation을 거친다
                Restaurant restaurant = Restaurant.builder()
                        .id(mapped_data.getId())
                        .address(mapped_data.getAddress())
                        .category(mapped_data.getCategory() == null ? "없음" : mapped_data.getCategory())
                        .imageUrl(mapped_data.getImageUrl() == null ? "" : URLDecoder.decode(mapped_data.getImageUrl(), "UTF-8"))
                        .name(mapped_data.getName())
//                        .distance(utility.stringToLongDistance(mapped_data.getDistance()))
                        .businessHours(mapped_data.getBusinessHours())
                        .visitorReviewScore(mapped_data.getVisitorReviewScore() == null ? 0.0 : Double.parseDouble(mapped_data.getVisitorReviewScore()))
                        .saveCount(utility.stringToLongSaveCnt(mapped_data.getSaveCount()))
                        .bookingReviewScore(mapped_data.getBookingReviewScore())
                        .restaurantType(type)
                        .x(mapped_data.getX())
                        .y(mapped_data.getY())
                        .build();
                entities.add(restaurant);
            }
            restaurantsRepository.saveAll(entities);
        }

    }
  • @BeforeEach 를 통해 먼저 DB에 스크래핑한 엔티티들을 삽입합니다
	@Test
    public void givenRestaurantID_whenFindallexist_thenInsertRestaurants() throws Exception{
        //given
        List<Restaurant> restaurants = restaurantsRepository.findAll();
        //when
        for (Restaurant entity : restaurants) {
            // findby id
            Restaurant newJoined = restaurantsRepository.findRestaurantById(entity.getId())
                    .orElse(restaurantsRepository.save(entity));
        }
        //then
    }
  • repository.findAll 을 통해 DB내의 모든 엔티티들을 담아줍니다 ( 레스토랑 정보가 있다고 가정 : given )
  • foreach 를 수행하여 해당 id 값을 가진 엔티티가 존재하지 않을 경우 엔티티를 save(insert) 해줍니다

로그확인하기
spring.jpa.properties.hibernate.show_sql=true 를 해두었기 때문에 쿼리 로그를 보면서 추가된 값이 있는지 확인합니다.

  1. 디버그 모드로 하여 중단점 잡기

    • 앞서 db에 저장하는 테스트를 진행한뒤 중단점을 잡고 따로 콘솔에서 몇개를 delete하여 정상적으로 없는 값일 경우 insert가 진행되는지 확인합니다
  2. 레스토랑 정보가 restaurants 변수에 담긴것을 확인한 뒤 콘솔에서 몇개를 골라 삭제합니다

    • 하이라이트된 값을 삭제해줍니다

    • 추가로 DB 내에서도 조회하여 확인 결과 잘 삭제되었음을 알 수 있습니다

    • cleanUp ( db 삭제 ) 가 일어나기 전에 중단점을 잡고 결과를 확인합니다

  3. 최종 확인


2. View 로 DTO 넘겨주기

유저 컨트롤러에서 xy 세션에 저장 후, 근처 음식점 갖고오기

UserController

	@PostMapping("/postXY")
    public String postXY(AddressDTO dto
            , @LoginUser SessionUser user){

        user.setX(dto.getX());
        user.setY(dto.getY());
        User newUser = userService.saveOrUpdateXY(user);
        httpSession.setAttribute("user", new SessionUser(newUser));

        return "redirect:/restaurant/addNearest";
    }

RestaurantController

@Controller
@RequestMapping("/restaurant")
public class RestaurantController {

    @Autowired
    private RestaurantService restaurantService;

    @Autowired
    private RestaurantsRepository restaurantsRepository;

    @SneakyThrows
    @GetMapping("/addNearest")
    public String addNearestRestaurant(@LoginUser SessionUser user){
        // 신규 주변 음식점 정보가 있다면 insert, 이후 restaurant view 로 이동
        GetRestaurantRequest request = GetRestaurantRequest.builder()
                                        .x(String.valueOf(user.getX()))
                                        .y(String.valueOf(user.getY()))
                                        .build();
        restaurantService.getRestaurantData(request);
        return "redirect:/restaurant/main"; // 메인 페이지로 이동
    }

    @GetMapping("/main")
    public String main(@LoginUser SessionUser user
                        ,Model model){
        // 현재 위치 기준 가까운 순으로 정렬
        AddressDTO request = AddressDTO.builder()
                .x(user.getX())
                .y(user.getY())
                .build();
        // 가까운 음식점 정보를 가져옴
        List<RestaurantDTO> datas = restaurantService.getRestaurantDTO(request);
        return "restaurant";
    }
}
  • 세션에 x,y 값 또한 저장해놓았기에 이를 바탕으로 근처 1키로 반경내의 음식점을 DB에서 조회합니다 - 관련 포스팅
  • DB 조회 이후 다시 한번 리다이렉트를 통해 main 으로 보내줍니다
  • 서비스를 통해 restaurantDTO 리스트를 받고 모델에 담아줍니다

RestaurantService

	@Override
    public List<RestaurantDTO> getRestaurantDTO(AddressDTO request) {
        // Point 간의 거리를 통하여 가까운 음식점 정보를 db 에서 매칭
        List<RestaurantDTOInterface> interfaces = restaurantsRepository.getRestaurantByLocation(request.getX(), request.getY());
        // interface -> dto 로
        List<RestaurantDTO> dtos = RestaurantDTO.interfaceToDto(interfaces);
        return dtos;
    }
  • 기존에 JPA를 통해서 조회했던 값은 인터페이스이기에 이를 DTO로 변환한뒤 보내주도록 합니다 - 관련포스팅

interfaceToDto

public static List<RestaurantDTO> interfaceToDto(List<RestaurantDTOInterface> dtoInterfaces){
        List<RestaurantDTO> dtos = new ArrayList<>();
        for (RestaurantDTOInterface dtoInterface : dtoInterfaces) {
            dtos.add(RestaurantDTO.builder()
                    .x(dtoInterface.getX())
                    .y(dtoInterface.getY())
                    .name(dtoInterface.getName())
                    .id(dtoInterface.getId())
                    .address(dtoInterface.getAddress())
                    .category(dtoInterface.getCategory())
                    .image_Url(dtoInterface.getImage_Url())
                    .diff_Distance(dtoInterface.getDiff_Distance())
                    .business_Hours(dtoInterface.getBusiness_Hours())
                    .visitor_Review_Score(dtoInterface.getVisitor_Review_Score())
                    .save_Count(dtoInterface.getSave_Count())
                    .booking_Review_Score(dtoInterface.getBooking_Review_Score())
                    .restaurant_Type(dtoInterface.getRestaurant_Type())
                    .build());
        }
        return dtos;
    }

피들러 확인

View

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <!--    <meta id="_csrf" name="_csrf" th:content="${_csrf.token}"/>-->
    <!--    <meta id="_csrf_header" name="_csrf_header" th:content="${_csrf.headerName}"/>-->
    <title>Title</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>

<h1>음식점 리스트</h1>
<div class="col-md-12">
    <!--    로그인 기능 영역 -->
    <div class="row">
        <div class="col-md-6">
            <table>
                <thead>
                    <tr>
                        <th>순번</th>
<!--                        <th>이미지</th>-->
                        <th>음식점명</th>
                        <th>대표메뉴</th>
                        <th>거리(m)</th>
                        <th>주소</th>
                        <th>영업시간</th>
                        <th>방문자리뷰</th>
                        <th>예약리뷰</th>
                        <th>카테고리</th>
                    </tr>
                </thead>
                <tbody>
                    <tr th:each="restaurantDTO, status:${restaurants}">
                        <td th:text="${status.index}"></td>
<!--                        <td><img id="imgId" th:src="${restaurantDTO.image_Url}" alt="첨부이미지" /></td>-->
                        <td th:text="${restaurantDTO.name}"></td>
                        <td th:text="${restaurantDTO.category}"></td>
                        <td th:text="${restaurantDTO.diff_Distance}"></td>
                        <td th:text="${restaurantDTO.address}"></td>
                        <td th:text="${restaurantDTO.business_Hours}"></td>
                        <td th:text="${restaurantDTO.visitor_Review_Score}"></td>
                        <td th:text="${restaurantDTO.booking_Review_Score}"></td>
                        <td th:text="${restaurantDTO.category}"></td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</div>
</body>
</html>

  • View 까지 잘 나오는 것 확인 ( 템플릿 적용이 험난하겠네요 )
  • 이미지의 경우 th:src 로 잘 출력되지만 사이징 처리까지 해줘야 하므로 보류중

Trouble Shooting
해당 결과를 보면 알 수 있듯 이미지 url 상에 인코딩 이슈가 있는것으로 보인다. 원인 제공처로 유추해볼 곳은 다음과 같다
1. 헤더에서의 인코딩 설정 - UTF_8 로 바꾸어 보았다 (X)

 public HttpHeaders getDefaultHeader(){
        MediaType mediaType = new MediaType("application", "json", Charset.forName("UTF-8"));
        HttpHeaders httpHeaders = new HttpHeaders();
        MultiValueMap<String, String> headerValues = new LinkedMultiValueMap<>();
        headerValues.add(HttpHeaders.ACCEPT, "*/*");
        headerValues.add(HttpHeaders.HOST, HOST_v2);
        headerValues.add(HttpHeaders.USER_AGENT, USER_AGENT);
        headerValues.add("Referer", REFERER);
        headerValues.add("Connection","keep-alive");
        httpHeaders.addAll(headerValues);
        httpHeaders.setContentType(mediaType);
        return httpHeaders;
    }
  1. restTemplate 의 인코딩 문제 - StringHttpMessageConverter 설정을 utf8로 바꾸었다 (X)
restTemplate.getMessageConverters()
        .add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
  1. response 에는 문제가 없음을 확인하고 매핑하는 구간에 로깅하여 확인해보았다

    매핑 전에는 문제없이 받아온다
  • 기존 매핑
Restaurant newJoined = restaurantsRepository.findRestaurantById(mapped_data.getId())
                        .orElse(restaurantsRepository.save(Restaurant.builder()
                                                                        .id(mapped_data.getId())
                                                                        .address(mapped_data.getAddress())
                                                                        .category(mapped_data.getCategory() == null ? "없음" : mapped_data.getCategory())
                                                                        .imageUrl(mapped_data.getImageUrl() == null ? "" : URLDecoder.decode(mapped_data.getImageUrl(), "UTF-8"))
                                                                        .name(mapped_data.getName())
                                                                        .businessHours(mapped_data.getBusinessHours())
                                                                        .visitorReviewScore(mapped_data.getVisitorReviewScore() == null ? 0.0 : Double.parseDouble(mapped_data.getVisitorReviewScore()))
                                                                        .saveCount(utility.stringToLongSaveCnt(mapped_data.getSaveCount()))
                                                                        .bookingReviewScore(mapped_data.getBookingReviewScore())
                                                                        .restaurantType(type)
                                                                        .x(mapped_data.getX())
                                                                        .y(mapped_data.getY())
                                                                        .build()));
  • URLDecoder.decode 를 사용하여 uricomponent 를 디코딩해주었는데 이곳에서 에러가 났음을 알 수 있었다 >> 변경 후 .imageUrl(mapped_data.getImageUrl() == null ? "" : mapped_data.getImageUrl())
  • 사실 디코딩하지 않아도 url 그대로 src 설정해주면 될텐데.. 그때는 무슨 생각이였는지 모르겠다

[결과]

0개의 댓글