본문 바로가기
Spring

Spring Data R2DBC 연관관계 구현 - ManyToOne

by Heesu.lee 2021. 4. 2.

지난 게시글은 OneToMany 연관관계 관계를 R2DBC 를 통해 어떤 식으로 구현할 수 있는지를 살펴보았다.

 

참고 - heesutory.tistory.com/34

 

Spring Data R2DBC 연관관계 구현 - OneToMany (2)

지난 번 작성된 R2DBC 에서 OneToMany 를 구현하는 부분을 이어서 작성한다. 지난 첫 게시글에선 DatabaseClient 로 별도의 Custom Repository 를 정의하였고, 해당 부분에서 Member 와 Order 를 JOIN 하여 처리하..

heesutory.tistory.com

 

이번 시간은 ManyToOne 관계를 한번 알아볼 수 있도록 해보겠습니다.

 

주문과 회원 ManyToOne 관계

 

이번 게시글에서도 회원(Member) 와 주문(Order) 를 사용하여 구현해보도록 하겠습니다.

연관관계 주인인 Order 입장에서 Member 와 Order 는 현재 ManyToOne 관계를 맺고 있습니다.

Member

// Member 
@Table
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Member {

    @Id
    @Column("member_id")
    private Long id;

    @Column("member_name")
    private String name;

    @Column("member_role")
    private Roles roles;

    @Transient
    private List<Order> orders;

    public Member update(List<Order> orders) {
        this.orders = orders;
        return this;
    }
}

// Member Repository
public interface MemberRepository extends ReactiveCrudRepository<Member, Long> {

}

Order

// Order
@Table("orders")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Order {

    @Id
    @Column("order_id")
    private Long id;

    @Column("member_id")
    private Long memberId;

    @Column("order_name")
    private String name;

    @Column("order_status")
    private OrderStatus status;

    @Transient
    private Member member;

}

// Order Repository
public interface OrderRepository extends ReactiveCrudRepository<Order, Long> {

}

 

기본적인 형태 엔티티와 레포지토리는 바로 위와 같이 구성되어있습니다.

Member 에서의 @Transient 어노테이션이 명시된 orders 리스트는 멤버와 주문의 OneToMany 관계를 풀어낸 것이고,

Order 에서의 @Transient 가 명시된 member 는 회원과의 ManyToOne 관계를 구현하고자 추가된 필드 입니다.

 

그렇다면 ManyToOne 은 어떤 식으로 구현해볼 수 있을까요?

R2DBC 에서 ManyToOne 을 구현하는 방식으로 다음 두가지가 존재합니다.

 

  • DatabaseClient 를 활용한 Custom Repository 구현 (+ Mapper 구현)
  • Query 어노테이션 + Covertor 정의

첫 번째 방식은 OneToMany 와 동일하게 Native SQL 를 정의하여 client 를 통해 직접 요청하고 맵핑해주는 방식입니다.

 

// Interface
public interface CustomOrderRepository {

    Flux<Order> findAllOrdersWithMember();
}

// Impl
@Repository
@RequiredArgsConstructor
public class CustomOrderRepositoryImpl implements CustomOrderRepository {

    private final DatabaseClient client;

    @Override
    public Flux<Order> findAllOrdersWithMember() {
        String query =
                "SELECT " +
                "member.member_id, member_name, member_role, " +
                "order_id, order_name, order_status " +
                "FROM orders " +
                "INNER JOIN member " +
                "ON orders.member_id = member.member_id";

        return client.sql(query)
                .map(mapper::apply)
                .all();
    }
}

// OrderRepository
public interface OrderRepository 
	extends ReactiveCrudRepository<Order, Long>, CustomOrderRepository {

    BiFunction<Row, RowMetadata, Order> MAPPER = (row, rowMetaData) -> Order.builder()
            .id(row.get("order_id", Long.class))
            .name(row.get("order_name", String.class))
            .status(OrderStatus.valueOf(row.get("order_status", String.class)))
            .memberId(row.get("member_id", Long.class))
            .member(Member.builder()
                    .name(row.get("member_name", String.class))
                    .id(row.get("member_id", Long.class))
                    .roles(Roles.valueOf(row.get("member_role", String.class)))
                    .build())
            .build();
}

 

Custom Repository 를 정의하고 해당 메서드를 DatabaseClient 를 통해서 정의해주면 됩니다.

여기서 살펴볼 부분은 지난번(OneToMany)과 다르게 Mapper 를 따로 정의하여 사용하고 있습니다.

 

현재는 Repository 에서 정의하였지만 실제 따로 클래스를 만들어 BiFunction 을 implement 하여 apply 메소드를 정의하여 사용할 수 있습니다.

public class OrderMapper implements BiFunction<Row, RowMetadata, Order> {

    @Override
    public Order apply(Row row, RowMetadata rowMetadata) {
        return Order.builder()
                .id(row.get("order_id", Long.class))
                .name(row.get("order_name", String.class))
                .status(OrderStatus.valueOf(row.get("order_status", String.class)))
                .memberId(row.get("member_id", Long.class))
                .member(Member.builder()
                        .name(row.get("member_name", String.class))
                        .id(row.get("member_id", Long.class))
                        .roles(Roles.valueOf(row.get("member_role", String.class)))
                        .build())
                .build();
    }
}

 

Test Code

@DataR2dbcTest
class CustomOrderRepositoryImplTest {

    @Autowired CustomOrderRepositoryImpl repository;

    @DisplayName("Custom Repository 주문 조회 테스트")
    @Test
    void findAllOrdersWithMemberTest() {
        // given

        // when
        List<Order> orders = repository.findAllOrdersWithMember()
                .collectList()
                .block();

        // then
        assertNotNull(orders);
        orders.forEach(order -> System.out.println("order = " + order));
    }
}

결과

테스트 결과

 

정상적으로 각 Order 에 필요한 Member 를 같이 가져오는 것을 확인할 수 있습니다.

Custom Converter 와 @Query 어노테이션을 활용한 방식은 다음에 다시 기재할 수 있도록 하겠습니다.

 

저번 게시글 부터 지금까지 R2DBC 를 활용한 연관관계 (ManyToOne, OneToMany) 를 어떻게 구현할 수 있는지를 확인하였습니다.

결과적으로 R2DBC 가 ORM 이 아니기에 개발자가 수동으로 Repository 나 Mapper 를 작성하지 않으면 해당 기능들을 사용할 수 없습니다.

 

 

참조

 

Handle R2dbc in Spring Projects

In this article, we will deepen what is Reactive programming, the place R2DBC specification takes in it and how developers can handle it in spring projects

www.sipios.com

 

※ 주니어 개발자이기에 부족한 점이 많습니다. 틀린 부분이나 부족한 부분에 대해 자유롭게 글 남겨주시면 감사하겠습니다.

댓글