지난 게시글은 OneToMany 연관관계 관계를 R2DBC 를 통해 어떤 식으로 구현할 수 있는지를 살펴보았다.
이번 시간은 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 를 작성하지 않으면 해당 기능들을 사용할 수 없습니다.
참조
※ 주니어 개발자이기에 부족한 점이 많습니다. 틀린 부분이나 부족한 부분에 대해 자유롭게 글 남겨주시면 감사하겠습니다.
'Spring' 카테고리의 다른 글
Spring WebFlux Error Handling (2) | 2021.04.12 |
---|---|
Spring Data R2DBC 연관관계 구현 - OneToMany (2) (0) | 2021.03.31 |
Spring Data R2DBC 연관관계 구현 - OneToMany (3) | 2021.03.28 |
Spring Data R2DBC 사용 (0) | 2021.03.28 |
Custom Validation 사용해보기 (0) | 2021.03.20 |
댓글