4 분 소요


다대다인 두 클래스 연관 관계 매핑

다대다의 관계는 중간에 테이블을 하나 추가해서 두 개의 일대다 관계를 만들어주는 것이 일반적인 방법이다.

다대다 관계를 매핑하면 연결 테이블이 자동으로 생성되고 복잡성이 증가한다.

  • 일대다 단방향 연관 관계

UBBdOc0fE-PNp9cZ2HUTJ-1657014449170.png

Member만 List 객체를 참조할 수 있으므로 단방향 관계

일대다 단방향 매핑은 잘 사용하지 않는다.



  • 두 개의 일대다 관계 (다대다)

qciw-mY-TdfuVPV0MQ7s9-1655373372531.png



1. Order 클래스(주문)와 Coffee 클래스(커피)의 연관 관계 매핑 구현

  • 주문과 커피는 다대다 관계이므로 OrderCoffee 클래스를 만든다
    • 다대다(N:M) 관계를 일대다-다대일(1:N-N:1) 관계로 매핑

OrderCoffee 클래스 추가

@NoArgsConstructor
@Getter
@Setter
@Entity
public class OrderCoffee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long orderCoffeeId;

    @ManyToOne
    @JoinColumn(name = "ORDER_ID")
    private Order order;

    @ManyToOne
    @JoinColumn(name = "COFFEE_ID")
    private Coffee coffee;

    @Column(nullable = false)
    private int quantity;

    public OrderCoffee(Order order, Coffee coffee, int quantity) {
        this.order = order;
        this.coffee = coffee;
        this.quantity = quantity;
    }

		public void setOrder(Order order) {
        this.order = order;
        if (!this.order.getOrderCoffees().contains(this)) {
            this.order.getOrderCoffees().add(this);
        }
    }

    public void setCoffee(Coffee coffee) {
        this.coffee = coffee;
        if (!this.coffee.getOrderCoffees().contains(this)) {
            this.coffee.setOrderCoffee(this);
        }
    }
}



Order 클래스 수정

  • OrderCoffee 리스트 추가, OneToMany 관계 설정
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderCoffee> orderCoffees = new ArrayList<>();

public void setMember(Member member) {
        this.member = member;
        if (!this.member.getOrders().contains(this)) {
            this.member.getOrders().add(this);
        }
    }

주의할 점은, cascade = CascadeType.ALL과 orphanRemoval = true를 설정해야 한다.

이 설정을 통해 Order 또는 Coffee 엔티티가 삭제될 때, 연결 엔티티인 OrderCoffee도 함께 삭제된다. 또한, OrderCoffee를 삭제할 때 연결된 Order 또는 Coffee 엔티티와의 연관 관계도 자동으로 제거된다.



Coffee 클래스 수정

@OneToMany(mappedBy = "coffee", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderCoffee> orderCoffeeList = new ArrayList<>();

public void setOrderCoffee(OrderCoffee orderCoffee) {
      this.orderCoffees.add(orderCoffee);
      if (orderCoffee.getCoffee() != this) {
          orderCoffee.setCoffee(this);
      }
  }




2. Member 클래스(회원)와 Stamp 클래스(도장)의 연관 관계 매핑 구현

Stamp 클래스 생성

@Getter
@Setter
@Entity
@NoArgsConstructor
public class Stamp {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long stampId;

    @Column(nullable = false)
    private int stampCount;

    @OneToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    public void setMember(Member member) {
        this.member = member;
        if (member.getStamp() != this) {
            member.setStamp(this);
        }
    }
}



Meber 클래스 수정(추가)

@OneToMany(mappedBy = "member", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
private Stamp stamp;

public void setStamp(Stamp stamp) {
    this.stamp = stamp;
    if (stamp.getMember() != this) {
        stamp.setMember(this);
    }
}




주문 기능 추가 구현

3. OrderService 클래스와 OrderController 기능 완성을 위한 추가 구현

  • 커피 주문 기능
    • OrderService 클래스의 createOrder() 메서드에서 주문한 커피가 테이블에 존재하는 커피인지 체크하는 기능 추가
    • 주문한 커피의 수량만큼 회원의 스탬프 숫자를 증가시켜야 함
  • 특정 주문 조회 기능
    • OrderController 클래스의 getOrder() 핸들러 메서드에서 주문한 커피 정보가 ResponseEntity에 포함되어야 함
  • 주문 목록 조회 기능
    • OrderController 클래스의 getOrders() 핸들러 메서드에서 주문한 커피 정보 목록이 ResponseEntity에 포함되어야 함



커피 주문 기능 / 스탬프 증가

OrderService 클래스 수정(추가)

private final CoffeeService coffeeService;

public OrderService(MemberService memberService,
                    OrderRepository orderRepository, CoffeeService coffeeService) {
    this.memberService = memberService;
    this.orderRepository = orderRepository;
    this.coffeeService = coffeeService;
}



커피가 존재하는지 확인

order.getOrderCoffees().stream()
                .forEach(orderCoffee -> coffeeService.findVerifiedCoffee(orderCoffee.getCoffee().getCoffeeId()));



updateStamp 추가

private int updateStamp(Order order){
    Member member = memberService.findMember(order.getMember().getMemberId());
    int stampCount = order.getOrderCoffees().stream()
            .map(orderCoffee -> orderCoffee.getQuantity())
            .mapToInt(quantity -> quantity)
            .sum();

    Stamp stamp = member.getStamp();
    stamp.setStampCount(stamp.getStampCount() + stampCount);

    memberService.updateMember(member);
    return stampCount;
}




특정 주문 조회 기능

OrderController 클래스의 getOrder() 핸들러 메서드 수정

@GetMapping("/{order-id}")
public ResponseEntity getOrder(@PathVariable("order-id") @Positive long orderId) {
    Order order = orderService.findOrder(orderId);

    return new ResponseEntity<>(new SingleResponseDto<>(mapper.orderToOrderResponseDto(order)), HttpStatus.OK);
}



주문 목록 조회 기능

OrderController 클래스의 getOrders() 핸들러 메서드 수정

@GetMapping
public ResponseEntity getOrders(@Positive @RequestParam int page,
                                @Positive @RequestParam int size) {
    Page<Order> pageOrders = orderService.findOrders(page - 1, size);
    List<Order> orders = pageOrders.getContent();

    return new ResponseEntity<>(
            new MultiResponseDto<>(mapper.ordersToOrderResponseDtos(orders), pageOrders), HttpStatus.OK);
}



OrderMapper 수정

주문한 커피 정보를 ResponseEntity에 포함시키기 위해서는 OrderMapper에서 OrderCoffee 와

OrderCoffeeResponseDto에 대한 추가 매핑이 필요하다.

@Mapper(componentModel = "spring")
public interface OrderMapper {
    default Order orderPostDtoToOrder(OrderPostDto orderPostDto){
        Order order = new Order();
        Member member = new Member();
        member.setMemberId(orderPostDto.getMemberId());

        List<OrderCoffee> orderCoffees = orderPostDto.getOrderCoffees().stream()
                .map(orderCoffeeDto -> {
                    OrderCoffee orderCoffee = new OrderCoffee();
                    Coffee coffee = new Coffee();
                    coffee.setCoffeeId(orderCoffeeDto.getCoffeeId());
                    orderCoffee.setOrder(order);
                    orderCoffee.setCoffee(coffee);
                    orderCoffee.setQuantity(orderCoffee.getQuantity());
                    return orderCoffee;
                }).collect(Collectors.toList());
        order.setMember(member);
        order.setOrderCoffees(orderCoffees);
        return order;
    }

    Order orderPatchDtoToOrder(OrderPatchDto orderPatchDto);

    List<OrderResponseDto> ordersToOrderResponseDtos(List<Order> orders);

    @Mapping(source = "member.memberId", target = "memberId")
    OrderResponseDto orderToOrderResponseDto(Order order);

    @Mapping(source = "coffee.coffeeId", target = "coffeeId")
    @Mapping(source = "coffee.korName", target = "korName")
    @Mapping(source = "coffee.engName", target = "engName")
    @Mapping(source = "coffee.price.value", target = "price")
    OrderCoffeeResponseDto orderCoffeeToOrderCoffeeResponseDto(OrderCoffee orderCoffee);
}




진행 과정 중 오류 사항

Caused by: org.hibernate.AnnotationException: Illegal attempt to map a non collection as a @OneToMany, @ManyToMany or @CollectionOfElements: com.codestates.member.entity.Member.stamp

Untitled

이 오류는 Hibernate가 @OnerToMany, @ManyToMany 또는 @CollectionOfElements와 같은 컬렉션 매핑 어노테이션을 사용하려는데, 해당 필드가 컬렉션 타입이 아니라 일반 객체 타입으로 매핑하려고 할 때 발생한다.

해결 법은 해당 필드를 컬렉션 타입으로 변경하거나, 올바른 어노테이션을 사용하여 매핑하는 것이다. 만약 해당 필드가 일반 객체 타입으로 유지해야 한다면, Hibernate의 매핑 어노테이션 중 하나를 사용하는 대신 직접 매핑하는 방법도 있다.

// 수정 전
@OneToMany(mappedBy = "member", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
private Stamp stamp;

// 수정 후
@OneToOne(mappedBy = "member", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
private Stamp stamp;

내 경우엔 @OneToOne 어노테이션이 오타로 인해 @OneToMany로 작성되있었다.




추가 학습

@Column 애너테이션 (필드와 컬럼을 매핑해주는 애너테이션)
(필드와 컬럼 차이 : 필드는 레코드 안에서 하나의 데이터 요소 / 컬럼은 여러 개의 레코드를 모아둔 열. 필드는 데이터의 속성을, 컬럼은 데이터의 형태를 결정하는데 중요한 역할을 함.)

@Column(nullable = false, updatable = false, unique = true)

  • nullable
    • 컬럼에 null 값을 허용할지 여부(디폴트 값 true)
  • updatable
    • 컬럼 데이터를 수정할 수 있는지 여부(디폴트 값 true)
  • unique
    • 하나의 컬럼에 유니크 제약 조건을 설정함 (디폴트 값 false)

@Column, @JoinColumn 차이는 @Column 애너테이션은 단순히 엔티티 클래스의 멤버 변수와 데이터베이스 테이블의 컬럼을 매핑할 때 사용하고, @JoinColumn 애너테이션은 연관된 엔티티 클래스와의 연결을 위한 외래 키 컬럼을 매핑할 때 사용한다.



cascade 애트리뷰트

@OneToMany, @ManyToOne 애너테이션에는 cascade 라는 애트리뷰트가 있다.

cascade 속성은 JPA에서 연관 엔티티를 저장하거나 삭제할 때 연쇄적으로 영향을 미치는 옵션이다.

이 속성을 사용하면 부모 엔티티의 상태 변화가 자식 엔티티에도 영향을 미친다.

예를 들어 @OneToMany or @ManyToOne 으로 매핑된 엔티티에서 cascade 속성을 사용했을때

부모 엔티티의 저장이나 삭제가 발생하면 자식 엔티티도 자동으로 저장이나 삭제가 된다.

cascade 속성

  • CascadeType.ALL: 모든 작업(저장, 수정, 삭제 등)에 대해 연쇄 작업이 적용된다.
  • CascadeType.PERSIST: 새로운 엔티티를 저장할 때 연쇄 작업이 적용된다.
  • CascadeType.MERGE: 엔티티를 병합할 때 연쇄 작업이 적용된다.
  • CascadeType.REMOVE: 엔티티를 삭제할 때 연쇄 작업이 적용된다.
  • CascadeType.REFRESH: 엔티티를 다시 로드할 때 연쇄 작업이 적용된다.
  • CascadeType.DETACH: 엔티티를 준영속 상태로 만들 때 연쇄 작업이 적용된다.

카테고리: ,

업데이트:

댓글남기기