com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of com.codesquad.airbnb.dto.BookingRequest (no Creators, like default constructor, exist)
Request DTO 클래스에 기본 생성자를 넣지 않으면 에러가 발생하는데 이유를 알고 싶다.
Json 데이터를 DTO 객체로 변환할 때 어떻게 동작하는지 알아봐야겠다.
(Why does jackson need default constructor 라는 키워드로 검색)
찾아보니 역직렬화, 즉 JSON 데이터를 객체로 만드는 과정에서 어떤 생성자를 갖고 있는지 모르기 때문에 기본 생성자를 호출한다. 그래서 기본 생성자가 필요한 것이다
숙소예약(Booking)을 POST 방식으로 추가하는 부분에서 에러 발생
java.sql.SQLIntegrityConstraintViolationException: Cannot add or update a child row:
a foreign key constraint fails (airbnb.booking, CONSTRAINT booking_ibfk_1 FOREIGN KEY (user_id) REFERENCES user (id))
nested exception is org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback;
SQL [insert into booking (room_id,user_id, check_in, check_out, number_of_people, total_price) values (?, ?, ?, ?, ?, ?)];
Cannot add or update a child row: a foreign key constraint fails (airbnb.booking, CONSTRAINT booking_ibfk_1 FOREIGN KEY (user_id) REFERENCES user (id));
request body로 user_id와 room_id 값을 넘겨준다.
에러 메시지상으론 user와 room에 존재하지 않는 id값을 넣어서 에러가 발생한다고 한다. 즉, 참조 무결성을 위반한 것.
이미 더미데이터로 넣은 user의 id와 room의 id값을 넣어주었기 때문에 에러가 발생한 원인이 이해가지 않았다.
하지만 객체 생성하는 부분에서 userId와 roomId를 반대로 인자를 넣어줘서 테이블에 존재하지 않는 userId로 외래키를 생성하려고 해서 에러가 났던 것이다.....
이렇게 비슷한 값을 두 개 이상 넣는 경우에는 꼼꼼히 코드를 짜는걸로
전체 숙소(Room)를 반환하는 기능과 검색조건으로 필터링 된 Room을 반환하는 기능의 URL을 같도록 API 설계했다.
그런데 프로그램을 실행하면, Invocation of init method failed; nested exception is java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'roomController' method 이라는 에러가 발생하고 프로그램이 종료된다.
같은 url로 GetMapping이 중복되어서 발생한 문제.
@RestController
@RequestMapping("/api/rooms")
public class RoomController {
...
@GetMapping
public Rooms showRooms() {
// 전체 Room을 반환하는 로직
...
}
@GetMapping(params = {"checkIn", "checkOut", "minPrice", "maxPrice", "numberOfPeople"})
public Rooms showFilteredRooms(@RequestParam("checkIn") LocalDate checkIn, @RequestParam("checkOut") LocalDate checkOut,
@RequestParam("minPrice") int minPrice, @RequestParam("maxPrice") int maxPrice,
@RequestParam("numberOfPeople") int numberOfPeople) {
// 검색 조건을 query string으로 받아서 필터링 된 Room을 반환하는 로직
...
}
이렇게 GetMapping 어노테이션에 params라는 속성을 추가하면 같은 url을 사용할 수 있다.
숙소예약(Booking)을 Post 방식으로 구현했다. 예약하려는 room에 날짜가 겹치는 booking이 있다면 예외발생, 없다면 insert 시키도록 했다.
기존에 더미데이터 세개만 두고 진행했을때는 잘 진행되는데, 똑같은 숙소 예약을 두번 넣으면 예외가 발생하지 않고, 똑같은 숙소 예약 데이터가 insert 된다.
기존에 넣어둔 더미 데이터 3개
그리고 같은 예약 두개를 추가하는 Request를 보내보면, id 4번 데이터와 5번 데이터를 보면 중복되게 들어간다.
예약을 추가하는 요청을 보내고 그 다음에 조회하는 쿼리를 날릴때 새로 삽입된 booking은 조회하지 않는건가 싶었다.
삽질을 하다가 쿼리에 문제가 있다는 것을 발견했다.
예약하려는 Room에 Booking들을 묶어서 날짜로 필터링 하도록 Room에 Booking 테이블을 조인 했다.
문제는 Room에 Booking을 조인 했더니, room에 해당하는 booking이 여러개 있을 경우에 하나의 booking만 조인된다. → booking에 room을 조인하도록 변경해야한다.
테스트 코드로 여러 상황을 테스트하지 않아서 뒤늦게 발견했다. 어쩌면 발견하지 못했을수도.. 테스트 케이스를 더 만들어봐야겠다.
Booking 클래스에서 체크인과 체크아웃을 LocalDate로 관리하고, Booking 테이블에서 해당 컬럼들은 date 타입으로 관리한다.
그리고 쿼리 스트링으로 체크인 체크아웃을 넣어서 요청을 보냈더니 MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.time.LocalDate'; 에러 발생
검색해보니 파라미터로 넘긴 값을 String으로 인식해서 TypeMismatchException이 발생한 것이다.
GetMapping 메소드의 파라미터에 @DateTimeFormat(pattern = "yyyy-MM-dd") 어노테이션을 붙여줘서 해결했다.
<추가>
RowMapper에서 resultSet.getDate()의 반환값은 java.sql.Date 자료형인데, toLocalDate 메소드를 이용하면 java.sql.date 에서 LocalDate 로 변환할 수 있다.
참고: https://perfectacle.github.io/2018/01/15/jackson-local-date-time-deserialize/
https://kouzie.github.io/jdbc/JDBC.-2일차/#javasqlstatement-로-select하기
@GetMapping 을 저렇게도 쓸 수 있었군요 처음 알아갑니다.