📝 Outline
- 마침내 Cart 페이지 구현이 완료되었다.
- 면접 준비로 인해 좀 늦어진 감이 있긴 하지만... (참 좋은 핑계거리야)
- 이번에 해당 페이지를 구현하면서 삽질한 것도 좀 있고 그 과정에서 나름 배운 것도 있어서 이를 하나씩 정리해보려고 한다.
- 구현 스타일은 다음과 같다.
- 그럼 바로 리뷰로 들어가보자.
📝 Review
💬 연관관계 매핑
- 이번에 Cart 페이지를 구현하면서 연관관계가 더 복잡해졌다.
- 우선 한 명의 고객은 하나의 장바구니만 가질 수 있기 때문에
Cart
와Customer
의 관계는 1:1로 설정해주었다. - 또한 하나의 장바구니에는 여러 개의 상품이 담길 수 있고, 하나의 상품은 여러 장바구니에 담길 수 있으므로,
Cart
와Product
의 관계는 N:M이 되어야 했다. 따라서 이는 1:N, N:1의 관계로 나누어 설계해주었다. - 이로써 추가된 엔티티는
Cart
,Customer
,ProductCart
가 되시겠다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Cart {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "cart")
private List<ProductCart> productCarts = new ArrayList<>();
@OneToOne
@JoinColumn(name = "customer_id")
private Customer customer;
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Customer {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String nickname;
private String phone;
private String address;
@Column(name = "membership_level")
private int membershipLevel;
private String status;
@OneToOne(mappedBy = "customer")
private Cart cart;
@Column(name = "register_date")
private LocalDateTime registerDate;
@Column(name = "modify_date")
private LocalDateTime modifyDate;
@PrePersist
public void registerDate() {
this.registerDate = LocalDateTime.now();
}
}
@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
public class ProductCart {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String size;
private int quantity;
@Column(name = "total_price")
private int totalPrice;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
@ManyToOne
@JoinColumn(name = "cart_id")
private Cart cart;
}
- 보면 알겠지만
ProductCart
객체는Product
와Cart
에서 각각@OneToMany
로 매핑되는 것을 확인할 수 있다. - 위처럼 설계하는게 정답은 아닐 수 있지만 우선은 이렇게 진행해볼 예정이다.
💬 CascadeType.REMOVE
- 이번에 장바구니 기능을 구현하면서 문득 떠오른 것이 있었다.
- 기존에 존재하던 상품이 판매 중지될 경우, 그 상품이 어느 고객의 장바구니에 담겨있었다면 어떻게 해야 할까?
- 당연히 그 장바구니에 담겨 있는 상품 정보도 함께 제거해야 할 것이다.
- 아하! 그럼
CascadeType.REMOVE
를 쓰면 되겠구나!
Product
를 아래와 같이 수정하고, 장바구니에 상품을 담은 뒤, 데이터베이스에서Product
개체를 삭제해보았다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product {
...
@OneToMany(mappedBy = "product", cascade = CascadeType.REMOVE)
private List<Size> sizes = new ArrayList<>();
@OneToMany(mappedBy = "product", cascade = CascadeType.REMOVE)
private List<Image> images = new ArrayList<>();
@OneToMany(mappedBy = "product", cascade = CascadeType.REMOVE)
private List<Feature> features = new ArrayList<>();
@OneToMany(mappedBy = "product", cascade = CascadeType.REMOVE)
private List<ProductCart> productCarts = new ArrayList<>();
...
}
- 결과는 다음과 같았다.
cannot delete or update a parent row a foreign key constraint fails
- 음... 외래키 제약조건 때문에 부모 개체의 행을 지울 수 없다고?
- 난 분명
@OneToMany
에cascade
옵션을 줬는데?!
- 문제를 해결하기 위해 구글링을 하다보니 나의 멍청함을 깨달을 수 있었다.
cascade
옵션은 JPA에 의해 처리되는 것으로 데이터베이스에서 직접 개체를 삭제하려고 하는 경우에는 적용되지 않는 것이었다.- 즉, JPA를 통해 코드 상으로
delete()
또는remove()
를 수행해주어야 제대로 동작하는 것이었다. - 그것도 모르고 아무런 CASCADE 조건도 걸려 있지 않은 데이터베이스에서
DELETE FROM product WHERE id = 1;
이러고 있었으니 오류가 날 수 밖에 - 그래도 확인은 해봐야 하니까 아래와 같이 메서드를 구현하고 테스트해보았다.
@RestController
public class ProductApiController {
@Autowired
private ProductService productService;
@DeleteMapping("product/delete/{product_id}")
public ResponseEntity<?> delete(@PathVariable Long product_id) {
productService.deleteProduct(product_id);
return new ResponseEntity<>(1, HttpStatus.OK);
}
}
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Transactional
public void deleteProduct(Long product_id) {
productRepository.deleteById(product_id);
}
}
- 결과는 성공적이었다.
💬 @OnDelete(action = OnDeleteAction.CASCADE)
- 위에서
CascadeType.REMOVE
를 통해 상품이 삭제되면 해당 상품과 연관되어 있는 모든 자식 개체를 지우도록 하였다. - 당연한 것이, 상품이 삭제될 경우 그 상품의 사이즈, 이미지, 특징 등과 같은 요소 역시 필요가 없어지기 때문이다.
- 다만 나 같은 경우 위처럼 할 경우 삭제를 위한 쿼리문이 너무 많이 나가게 된다.
- 그도 그럴것이 전부다
@OneToMany
로 매핑되어 있기 때문에 상품 하나가 삭제됐을 때 그와 연관되어 있는 모든 Many가 삭제되기 때문이다. - 따라서 뭔가 다른 방법이 필요했고, 결국 찾은 방법이 바로
@OnDelete(action = OnDeleteAction.CASCADE)
이다. CascadeType.REMOVE
와@OnDelete(action = OnDeleteAction.CASCADE)
의 가장 큰 차이는, JPA에 의해 처리되느냐, DDL에 의해 DB단에서 처리되느냐이다.CascadeType.REMOVE
- 전자의 방식을 취할 경우, JPA에 의해 외래 키를 찾아가며 참조하는 레코드를 제거해주게 된다.
- 따라서, JPA 상에서는 참조하고 있는 레코드의 개수만큼
delete
쿼리가 생성된다.
@OnDelete(action = OnDeleteAction.CASCADE)
- 후자의 방식을 취할 경우, 데이터베이스 자체에서
on delete cascade
제약조건이 걸리게 되며, 이를 통해 참조하는 레코드가 모두 제거되는 것이다. - 따라서, JPA 상에서는 한 개의
delete
쿼리가 생성되고, 데이터베이스에서 이를 처리해준다.
- 후자의 방식을 취할 경우, 데이터베이스 자체에서
- 자세한 내용은 여기에서..
'🚗 Backend Toy Project > Baeg-won Clothing Gallery' 카테고리의 다른 글
[Baeg-won Clothing Gallery] 9. PROFILE 페이지 구현 (0) | 2023.07.31 |
---|---|
[Baeg-won Clothing Gallery] 8. WISH 페이지 구현 (0) | 2023.07.28 |
[Baeg-won Clothing Gallery] 6. DETAIL 페이지 구현 (0) | 2023.07.16 |
[Baeg-won Clothing Gallery] 5. SALE, CONTACT 페이지 구현 (0) | 2023.07.15 |
[Baeg-won Clothing Gallery] 4. CLOTHING, FOOTWEAR, ACCESSORIES 페이지 구현 (0) | 2023.07.14 |