오랜만에 시간가는지 모르고 공부한 것 같다.

어제 다대다 관계에 대한
서로의 객체 참조가 이해가지 않아 새벽 3시까지
밤을 새워가며 공부를 했다.
졸리기도 했지만 알고자하는 마음에 집중이 오랜만에 엄청 된 것 같다.

어제 공부했더 다대다(N:M) 관계에 대해 공부해보자


다대다 관계 (N:M)

다대다 관계는 N:N관계라고 한다.
예젠에 RDBMS에서 공부 했던 것 처럼 중간에 테이블을 한개
더 만들어서 1 : N : 1 로 만들어주는 방법이다.

image

위와 같이 간단한 예제로 손님과, 주문을 다대다 관계로 맵핑하고 싶을 경우
중간에 Customer_Order라는 테이블에 각자의 외래키를 두어
참조할 수 있게 하는 것이다.

위에 그림처럼 중간에 테이블 클래스를 하나 만들어
두 테이블의 ID값을 참조하면 되는 방법이다.


예를 들기 위해
Coffee와 Order 클래스를 만들어서 진행 하려한다.
고객이라는 클래스는 빠졌지만 간단하게 하기위해
두가지만 만들고 진행해보자

@ManyToOne 단방향

Coffee는 커피의 등록정보라고하자 (커피이름,가격 등)
Order은 사용자가 주문한 정보라고하자 (주문시간, 고객이름 등)
서로 다대다 관계를 맺어 중간테이블을 통해 객체 참조를 해보자

@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "COFFEE")
public class Coffee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long coffeeId;

    @Column(nullable = false)
    private String coffeeName;

    @Column(nullable = false)
    private int price;
}
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "ORDERS")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long orderId;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();

    @Column
    private String personName;
}
@Entity
@Getter
@Setter
@NoArgsConstructor
public class OrderCoffee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long orderCoffeeId;

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

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

Order - OrderCoffee - Coffee (1:N:1)으로
이루어진 관계이다. 현재 OrderCoffee에서만
@ManyToOne으로 다대일로 연관관계를 맺고 있다.

이렇게 설정을 할 경우
OrderCoffee 테이블애서 coffee,order의 참조가 가능하지만
Order와 Coffee는 서로 참조를 하지 못한다.

위와 같은 경우에는 OrderCoffee 객체 타입으로
Repository를 만들어 저장해주어야한다.

public interface OrderCoffeeRepository extends JpaRepository<OrderCoffee,Long> {
}
@Service
public class OrderCoffeeService {
    private final OrderCoffeeRepository orderCoffeeRepository;

    public OrderCoffeeService(OrderCoffeeRepository orderCoffeeRepository) {
        this.orderCoffeeRepository = orderCoffeeRepository;
    }

    public OrderCoffee createOrderCoffee(OrderCoffee orderCoffee){
        return orderCoffeeRepository.save(orderCoffee);
    }
}

OrderCoffee 클래스 타입으로 만든 객체에
coffee,order 객체를 setter로 주입시켜준 다음
해당 객체를 매개변수로 가진 createOrderCoffee 메서드를 호출하면
JpaRepository로 인해 DB에 값이 저장되게 되어진다.

이렇게되면 OrderCoffee 테이블에는 order,coffe의 ID값으로 외래키로
지정되어 각각 객체를 참조하여 사용이 가능하다.

image


@ManyToOne @OneToMany 양방향

하지만 !!
우리는 Order와 Coffee의 다대다 관계를 보고 있는데
위와 같이 사용할 경우 Order에서 Coffee를 참조하지 못하게 되는 상황이다.

서로 참조가 가능하도록 코드를 수정해보자

@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "ORDERS")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long orderId;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();

    @Column
    private String personName;

    @OneToMany(mappedBy = "order")
    private List<OrderCoffee> orderCoffees = new ArrayList<>();
}
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "COFFEE")
public class Coffee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long coffeeId;

    @Column(nullable = false)
    private String coffeeName;

    @Column(nullable = false)
    private int price;

    @OneToMany(mappedBy = "coffee")
    private List<OrderCoffee> orderCoffees = new ArrayList<>();
}

Order과 Coffee를 @OneToMany를 이용해
양방향으로 참조가 가능하도록 설정해 주었다.

이렇게 설정할 경우 Order가 OrderCoffee 참조가 가능하고
그로인해 OrderCoffee에서 Coffee로 참조가 가능해진다.

예를 들어 우리가
매장에 커피가 3개가 등록되어있다고해보자 (아메리카노,카페라떼,헤이즐넛라떼)

image

Coffee Repository를 이용해 DB에 저장한 모습이다.

나라는 사람이 1개의 주문을 했는데 등록된 커피중 아메리카노를 주문했다고 가정해보자
그러면 Order Controller 핸들러메서드를 통해 POST 주문을 해야할 것이다.
나라는 사람의 정보과 어떤 커피인지만 알면 된다.
즉, 주문(Order)을 하면 OrderCoffee 테이블까지 맵핑을 시켜줘야한다.

위의 내용을 토대로
주문 Controller,Service를 만들었다.
차근차근 아래에서 확인해보자

우선 핸들러 메서드를 맵핑해줄 Dto를 만들자
위에서 얘기했듯이 주문하는 사람이름과, 커피 목록의 번호만 알면 된다.

@Getter
@Setter
public class OrderPostDto {
    private String personName;
    private List<OrderCoffeeDto> orderCoffees;
}
@Getter
@Setter
public class OrderCoffeeDto {
    private long coffeeId;
}

위와 같이 Dto를 작성했고

image

Postman을 통해 API 요청을 위와 같이 보내면 된다.
이렇게 보내주었을 경우 우리는 coffeeId 1번 즉, 아메리카노라는 것을
알 수 있고, 주문한사람은 “이재혁”이 되는 것이다.

이어서 Controller을 작성해보자

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping
    public ResponseEntity postOrder(@RequestBody OrderPostDto orderPostDto){

        Order order = new Order();

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

        order.setPersonName(orderPostDto.getPersonName());
        order.setOrderCoffees(orderCoffees);

        Order result = orderService.createOrder(order);

        return new ResponseEntity(HttpStatus.CREATED);
    }
}

Mapper를 이용하지 않고 Controller에다가 프로그램을 작성했다.
연습용으로만 하는 것이기에 참고 부탁바란다.

내용을 보면

  1. Order,Coffee 객체로 새로 만들어 주고 있다.

  2. OrderCoffee 객체를 만들어 order,coffee 객체를 setter로 주입하고 있다.
    -> 그중 Coffee객체는 Dto로 받은 coffeeId를 넣어서 만든다.

  3. OrderCoffee 객체를 List로 반환해준다.

  4. Order 객체의 필드변수에 값을 주입하고있다.
    -> 반환한 객체를 Order 객체의 필드변수 orderCoffees에 주입해주고 있다.
    -> Dto로 받은 값을 personName에 주입해주고 있다.

  5. 최종 적으로 값이 주입된 Order 객체를 Service 로직으로 전달한다.

여기서 알 수 있는 점은
우리가 Order 클래스에 양방향으로 지정한 @OneToMany List<OrderCoffee> orderCoffees
객체에 OrderCoffee 클래스의 값들이 List형태로 존재하게 된다.

Order 클래스에서 별도의 findOrderCoffee 쿼리를 작성하지 않더라고
OrderCoffee의 List화 되어있는 객체를 참조하여 Coffee를 조회할 수 있다.


이제 Service와 Repository 코드를 작성해보자

@Service
public class CoffeeService {
    private final CoffeeRepository coffeeRepository;

    public CoffeeService(CoffeeRepository coffeeRepository) {
        this.coffeeRepository = coffeeRepository;
    }

    public Coffee createCoffee(Coffee coffee){
        return coffeeRepository.save(coffee);
    }
}
public interface OrderRepository extends JpaRepository<Order, Long> {
}

간단하게 JpaRepository를 상속받아 JDBC와 같이
쿼리문을 작성할 수도있고 정해진 규약대로 쿼리를 날릴 수도 있다.
그중 .save();를 이용해 Order 객체를 DB에 저장하게 해주었다.

이제 주문을 Postman으로 실행하게 될 경우

image

위와 같이 주문이 잘 DB에 저장된 모습을 볼 수 있다.


하지만 ! 여기서 OrderCoffee의 테이블에는

image

값이 저장되지 않는 모습을 볼 수 있다.

왜냐하면 우리는 Order Entity에 JPA기술을 이용해
ORDER TABLE에만 값을 .save(); 해주었기 때문이다.

여기서 @ManyToOne, @OneToMany의 속성중
Cascade라는 것으로 간단하게 해결할 수 있다.

Order 클래스의 Cascade 속성을 추가해주면된다.

@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "ORDERS")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long orderId;

    @Column(nullable = false)
    private LocalDateTime createdAt = LocalDateTime.now();

    @Column
    private String personName;

    @OneToMany(mappedBy = "order",cascade = CascadeType.PERSIST)
    private List<OrderCoffee> orderCoffees = new ArrayList<>();
}

위와 같이 CascadeType.PERSIST 속성을 사용하면
Order객체가 .save();되어질때 ORDER_COFFEE 테이블에도
값이 같이 맵핑되게 되어진다.

Cascade란?
cascade 옵션이란 @OneToMany 나 @ManyToOne에 옵션으로 줄 수 있는 값이다.
Entity의 상태 변화를 전파시키는 옵션이다.
만약 Entity의 상태 변화가 있으면 연관되어 있는(ex. @OneToMany, @ManyToOne)
Entity에도 상태 변화를 전이시켜준다.

  1. Persistent: 저장을 하고나서, JPA가 아는 상태(관리하는 상태)가 된다.
    그러나 .save()를 했다고 해서, 이 순간 바로 DB에 이 객체에 대한 데이터가 들어가는 것은 아니다.
    JPA가 persistent 상태로 관리하고 있다가, 후에 데이터를 저장한다.
    (1차 캐시, Dirty Checking(변경사항 감지), Write Behind(최대한 늦게, 필요한 시점에 DB에 적용) 등의 기능을 제공한다)

  2. Removed: JPA가 관리하는 상태이긴 하지만, 실제 commit이 일어날 때, 삭제가 일어난다.

Order 클래스의 코드를 수정 후
다시 Postman을 통해 주문요청을 보낼 경우

image

비로소 ORDER_COFFEE 테이블에 외래키로
값들이 저장되어지는 모습을 볼 수 있다.
이렇게 연관관계를 통해 DB에 맵핑과 서로 객체 참조가 가능하다.



이렇게 오늘은 다대다에 대해 정리해보았다.
@ManyToMany는 실무에 잘사용하지 않는다 들어서
아직 공부하지 못한 상태이긴하다..
사실 위의 방법으로 연관관계 맵핑하는 것은 간단하다고 생각한데
서로 객체를 참조한다는? 의미가 잘 와닿지않기는한다.

계속 사용해보면서 익숙해질 수 밖에 !!

오늘 공부는 여기까지


오늘의 커피량: ☕️ ☕️ ☕️
오늘의 점심: 김치찌개라면