예를 들어 Member 클래스와 Order 클래스 관계에서 Member에만 Order 객체를 원소로 포함하는 List 객체를 가지고 있으면, Order를 참조할 수 있습니다.
하지만 이 경우, Order 클래스는 Member 클래스에 대한 참조 값이 없으므로 Order 입장에서는 Member 정보를 알 수 없습니다.
이처럼 한쪽 클래스만 다른 쪽 클래스의 참조 정보를 가지고 있는 관계를 단방향 연관 관계라고 합니다.
이번에는 위의 경우와는 다르게
Member 클래스가 Order 객체를 원소로 포함하고 있는 List 객체를 가지고 있고, 그러므로 Order 클래스를 참조할 수 있고, Member는 Order의 정보를 알 수 있습니다.
그리고 Order 클래스 역시 Member 객체를 가지고 있으므로, Member 클래스를 참조할 수 있습니다.
결론적으로 두 클래스가 모두 서로의 객체를 참조할 수 있으므로, Member는 Order 정보를 알 수 있고, Order는 Member 정보를 알 수 있습니다.
이처럼 양쪽 클래스가 서로의 참조 정보를 가지고 있는 관계를 양방향 연관 관계라고 합니다.
JPA는 단방향 연관 관계와 양방향 연관 관계를 모두 지원하는 반면에 Spring Data JDBC는 단방향 연관 관계만 지원합니다.
일대다의 관계란 일(1)에 해당하는 클래스가 다(N)에 해당하는 객체를 참조할 수 있는 관계를 의미합니다.
(일단, 일대다 단방향 매핑은 잘 사용하지 않습니다.)
Member와 Order는 일대다 관계이며, 단방향 관계라는 가정을 하겠습니다.
테이블 간의 관계에서는 일대다 중에서 ‘다’에 해당하는 테이블에서 ‘일’에 해당하는 테이블의 기본키를 외래키로 가집니다.
따라서 ORDERS 테이블이 MEMBER 테이블의 기본키인 member_id를 외래키로 가집니다.
그런데 Order 클래스가 ‘테이블 관계에서 외래키에 해당하는 MEMBER 클래스의 참조값’을 가지고 있지 않기 때문에 일반적인 테이블 간의 관계를 정상적으로 표현하지 못하고 있습니다.
따라서, Order 클래스의 정보를 테이블에 저장하더라도 외래키에 해당하는 MEMBER 클래스의 memberId 값이 없는 채로 저장이 됩니다.
이러한 문제 때문에 일대다 단방향 매핑은 잘 사용하지 않습니다.
일대다 단방향 매핑 하나만 사용하는 경우는 드물고,
다대일 단방향 매핑을 먼저 한 후에 필요한 경우, 일대다 단방향 매핑을 추가해서 양방향 연관 관계를 만드는 것이 일반적입니다.
다대일의 관계란 다(N)에 해당하는 클래스가 일(1)에 해당하는 객체를 참조할 수 있는 관계를 의미합니다.
다대일 단방향 매핑은 ORDERS 테이블이 MEMBER 테이블의 member_id를 외래키로 가지듯이 Order 클래스가 Member 객체를 외래키처럼 가지고 있습니다.
즉, 다대일 단방향 매핑은 테이블 간의 관계처럼 자연스러운 매핑 방식이기 때문에 JPA의 엔티티 연관 관계 중에서 가장 기본으로 사용되는 매핑 방식입니다.
@NoArgsConstructor
@Getter
@Setter
@Entity(name = "ORDERS")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long orderId;
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus = OrderStatus.ORDER_REQUEST;
@Column(nullable = false)
private LocalDateTime createdAt = LocalDateTime.now();
@Column(nullable = false, name = "LAST_MODIFIED_AT")
private LocalDateTime modifiedAt = LocalDateTime.now();
@ManyToOne // (1)
@JoinColumn(name = "MEMBER_ID") // (2)
private Member member;
.
.
.
다대일의 연관관계 매핑에서는
먼저 (1)과 같이 @ManyToOne 애너테이션으로 다대일의 관계를 명시합니다.
그리고 (2)와 같이 @JoinColumn 애너테이션으로 ORDERS 테이블에서 외래키에 해당하는 열 이름을 적어줍니다.
다대일 단방향 연관 관계이라면 다(N) 쪽에서만 설정을 해주면 매핑 작업은 끝납니다.
@Configuration
public class JpaManyToOneUniDirectionConfig {
private EntityManager em;
private EntityTransaction tx;
@Bean
public CommandLineRunner testJpaManyToOneRunner(EntityManagerFactory emFactory) {
this.em = emFactory.createEntityManager();
this.tx = em.getTransaction();
return args -> {
mappingManyToOneUniDirection();
};
}
private void mappingManyToOneUniDirection() {
tx.begin();
Member member = new Member("hgd@gmail.com", "Hong Gil Dong",
"010-1111-1111");
// (1)
em.persist(member);
Order order = new Order();
order.addMember(member); // (2)
em.persist(order); // (3)
tx.commit();
// (4)
Order findOrder = em.find(Order.class, 1L);
// (5) 주문에 해당하는 회원 정보를 가져올 수 있다.
System.out.println("findOrder: " + findOrder.getMember().getMemberId() +
", " + findOrder.getMember().getEmail());
}
}
우리가 ORDER 테이블에 주문 정보를 저장하는 INSERT 쿼리문에는 MEMBER 테이블의 MEMBER_ID가 외래키로 포함이 될 것입니다.
(2)와 같이 추가되는 member 객체는 이 MEMBER_ID 같은 외래키의 역할을 한다고 생각하면 됩니다.
(5)에서 findOrder.getMember().getMemberId()와 같이 객체를 통해 다른 객체의 정보를 얻을 수 있는 것을 객체 그래프 탐색이라고 합니다.
카페 주인 입장에서는 이 주문을 누가 했는지 주문한 회원의 회원 정보를 알아야 할 경우에는 다대일 매핑을 통해 주문한 사람의 정보를 조회할 수 있습니다.
그런데 회원 입장에서는 내가 주문한 주문의 목록을 확인할 수 있어야 할 텐데 다대일 매핑만으로는 member 객체를 통해 내가 주문한 주문 정보인 order 객체들을 조회할 수 없습니다.
이 경우, 다대일 매핑이 되어 있는 상태에서 일대다 매핑을 추가해 양방향 관계를 만들어주면 됩니다.
@NoArgsConstructor
@Getter
@Setter
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
@Column(nullable = false, updatable = false, unique = true)
private String email;
@Column(length = 100, nullable = false)
private String name;
@Column(length = 13, nullable = false, unique = true)
private String phone;
@Column(nullable = false)
private LocalDateTime createdAt = LocalDateTime.now();
@Column(nullable = false, name = "LAST_MODIFIED_AT")
private LocalDateTime modifiedAt = LocalDateTime.now();
// (1)
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
public Member(String email) {
this.email = email;
}
public Member(String email, String name, String phone) {
this.email = email;
this.name = name;
this.phone = phone;
}
public void addOrder(Order order) {
orders.add(order);
}
}
일대다 단방향 매핑의 경우에는 mappedBy 애트리뷰트의 값이 필요하지 않습니다.
mappedBy는 참조할 대상이 있어야 하는데 일대다 단방향 매핑의 경우 참조할 대상이 없으니까요.
mappedBy의 값은 관계를 소유하고 있는 필드를 지정하는 것으로 이해할 수 있습니다.
MEMBER 테이블과 ORDER 테이블의 관계에서 ORDER 테이블의 외래키로 MEMBER 테이블의 기본키 열인 MEMBER_ID의 값을 지정합니다.
그렇다면 Order 클래스에서 외래키의 역할을 하는 필드는
바로 member 필드입니다.
그렇기 때문에 mappedBy의 값이 “member”가 되는 것입니다.
mappedBy의 값으로 무얼 지정해야 하나?
(1) 두 객체들 간에 외래키의 역할을 하는 필드는 무엇인가?
(2) 외래키의 역할을 하는 필드는 다(N)에 해당하는 클래스 안에 있다.
주문(Order)과 커피(Coffee)의 관계는 다대다 관계입니다.
하나의 주문에 여러 개의 커피가 속할 수 있고, 하나의 커피는 여러 주문에 속할 수 있으니 다대다 관계인 것입니다.
JPA에서 다대다에 해당하는 엔티티 클래스는 테이블 설계 시, 중간에 테이블을 하나 추가해서 두 개의 일대다 관계를 만들어주는 것이 일반적인 방법입니다.
일대일 연관 관계 매핑은 다대일 단방향 연관 관계 매핑과 매핑 방법은 동일합니다.
단지 @ManyToOne 애너테이션이 아닌 @OneToOne 애너테이션을 사용한다는 차이만 있습니다.
일대일 단방향 매핑에 양방향 매핑을 추가하는 방법도 다대일에 일대다 매핑을 추가하는 방식과 동일합니다.
단, @OneToOne 애너테이션을 사용합니다.