엔티티 들은 대부분 다른 엔티티와 연관 관계를 맺고 잇습니다. JPA에서는 엔티티에 연관관계를 매핑해두고 필요할 때
해당 엔티티와 연관된 엔티티를 사요아여 좀 더 객체지향적으로 프로그래밍할 수 있도록 도와줍니다.
연관 관계 매핑의 기초를 알아보겠습니다.
예제 코드들은 쇼핑물 프로젝트 코드 입니다.
http://www.yes24.com/Product/Goods/103453774
첫 번째로 기억해야 할 것은 연관 관계 매핑의 종류 입니다. 4가지의 매핑 관계가 있습니다.
- 일대일(1:1):@OneToOne
- 일대다(1:N) @OnetoMany
- 다대일(N:1)@ManyToOne
- 다대다(N:N)@ManyToMany
두번째로 중요한 것은 엔티티를 맵핑할 때는 방향성을 고려해야 합니다. 테이블에서 관계는 항상 양방향이지만, 객체에서는 단방향과 양방향이 존재합니다.
- 단방향
- 양방향
일대일 단방향 매핑
Cart
package com.shop.entity;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.persistence.*;
@Entity
@Table(name = "cart")
@Getter
@Setter
@ToString
public class Cart {
@Id
@Column(name = "cart_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@OneToOne
@JoinColumn(name = "member_id")
private Member member;
}
- @OneToOne 어노테이션을 이용해 회원 엔티티와 일대일 매핑을 합니다.
- @JoinCoulmn 어노테이션을 이용해 매핑할 외래키를 지정합니다. name 속성에는 매핑할 외래키의 이름을 설정합니다. @JoinColumn의 name을 명시하지 않으면 JPA가 알아서 ID를 찾지만 컬럼명이 원하는 대로 생성되지 않을 수 있기 때문에 직접 지정하겠습니다.
소스코드를 다 작성한 후 회원(Member) 엔티티를 보면 회원 엔티티에는 장바구니(Cart) 엔티티와 관련된 소스가 전혀 없다는 것을 확인하셨을 것입니다. 즉, 장바구니 엔티티가 일방적으로 회원 엔티티를 참조하고 있습니다. 장바구니와 회원은 일대일로 매핑돼 있으며, 장바구니 엔티티가 회원 엔티티를 참조하는 일대일 단방향 매핑입니다.
애플리케이션을 실행하면 콘솔창에 cart테이블이 생성되는 쿼리문이 실행되는 것을 볼 수 있습니다.
cart 테이블은 member_id 컬럼을 외래키(foreign key) 로 갖습니다. 테이블으르 먼저 생성하는 쿼리문이 실행되고 member_id를 foreign key로 지정하는 쿼리문이 실행됩니다.
장바구니 엔티티와 회원 엔티티의 매핑이 완료됐습니다. 이렇게 매핑을 맺어주면 장바구니 엔티티를 조회하면서 회원 엔티티의 정보도 동시에 가져올 수 있는 장점이 있습니다.
장바구니 엔티티 조회 테스트를 하는 레포지토리를 만들겁니다.
CartRepository
package com.shop.repository;
import com.shop.entity.Cart;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CartRepository extends JpaRepository<Cart,Long> {
}
CartTest
package com.shop.entity;
import com.shop.dto.MemberFormDto;
import com.shop.repository.CartRepository;
import com.shop.repository.MemberRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.TestPropertySource;
import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceContext;
import javax.transaction.Transactional;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
@Transactional
@TestPropertySource(locations="classpath:application-test.properties")
public class CartTest {
@Autowired
CartRepository cartRepository;
@Autowired
MemberRepository memberRepository;
@Autowired
PasswordEncoder passwordEncoder;
@PersistenceContext
EntityManager em;
public Member createMember(){
MemberFormDto memberFormDto = new MemberFormDto();
memberFormDto.setEmail("tset@email.com");
memberFormDto.setName("홍길동");
memberFormDto.setAddress("서울시 마포구 합정동");
memberFormDto.setPassword("1234");
return Member.createMember(memberFormDto,passwordEncoder);
}
@Test
@DisplayName("장바구니 회원 엔티티 매핑 조회 테스트")
public void findCartAndMemberTest(){
Member member = createMember();
memberRepository.save(member);
Cart cart = new Cart();
cart.setMember(member);
cartRepository.save(cart);
em.flush();
em.clear();
Cart savedCart = cartRepository.findById(cart.getId())
.orElseThrow(EntityNotFoundException::new);
assertEquals(savedCart.getMember().getId(),member.getId());
}
}
- 회원 엔티티를 생성하는 메소드를 만듭니다.
- JPA는 영속성 컨텍스트에 데이터를 저장 후 트랜잭션이 끝날 때 flush()를 호출하여 데이터베이스에 반영합니다. 회원 엔티티와 장바구니 엔티티를 영속성 컨텍스트에 저장 후 엔티티 매니저로부터 강제로 flush()를 호출하여 데이터베이스에 반영합니다.
- JPA는 영속성 컨텍스트로부터 엔티티를 조회 후 영속성 컨텍스트에 엔티티가 없을 경우 데이터베이스를 조회합니다. 실제 데이터베이스에서 장바구니 엔티티를 가지고 올 때 회원 엔티티도 같이 가지고오는지 보기 위해서 영속성 컨텍스트를 비워주겠습니다.
- 저장된 장바구니 엔티티를 조회합니다.
- 처음에 저장한 member 엔티티의 id와 saverdCart에 매핑된 member 엔티티의 id를 비교합니다.
테스트 코드를 디버깅 모드로 실행 후 하나씩 따라가보면 코드에서 장바구니와 회원 데이터를 insert하는 쿼리문이 콘솔창에 출력되는 것을 볼 수 있습니다. 쓰기 지연 SQL 저장소에 저장된 쿼리문이 데이터베이스에 반영됩니다.
코드를 실행할 때는 cart테이블과 member 테이블을 조인해서 가져오는 쿼리문이 실행됩니다. cart 엔티티를 조회하면서 member 엔티티도 동시에 가져오는 것입니다.
엔티티를 조회할 때 해당 엔티티와 매핑된 엔티티도 한 번에 조회하는 것을 '즉시 로딩' 이라고 합니다.
일대일(@OnewToOne), 다대일(@ManyToOne)로 매핑할 경우 즉시 로딩을 기본 Fetch 전략으로 설정합니다.
Cart.java 클래스에서 member 엔티티와 일대일 매핑 관게를 맺어줄 때 따로 옵션을 주지 않으면 아래 코드와 같이 FetchType.EAGER(즉시 로딩)로 설정하는 것과 동일합니다.
@OneToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "member_id")
private Member member;
다대일 단방향 매핑
CartItem
package com.shop.entity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Getter
@Setter
@Table(name = "cart_item")
public class CartItem {
@Id
@GeneratedValue
@Column(name = "cart_item_id")
private Long id;
@ManyToOne
@JoinColumn(name = "cart_id")
private Cart cart;
@ManyToOne
@JoinColumn(name = "item_id")
private Item item;
private int count;
}
- 하나의 장바구니에는 여러 개의 상품을 담을 수 있으므로 @ManyToOne 어노테이션을 이용하여 다대일 관계로 매핑합니다.
- 장바구니에 담을 상품의 정보를 알아야 하므로 상품 엔티티를 매핑해줍니다. 하나의 상품은 여러 장바구니의 장바구니 상품으로 담길 수 있으므로 마찬가지로@ManyToOne 어노테이션을 이용하여 다대일 관계로 매핑합니다.
- 같은 상품을 장바구니에 몇 개 담을지 저장합니다
장바구니 상품 도메인 설계가 끝났으므로 에플리케이션을 재실행하여 콘솔창에 출력되는 쿼리문을 보겠습니다.
cart_item 테이블에 @JoinColumn어노테이션에 name으로 설정한 값이 컬럼 id로 추가됩니다.
엔티티와 매핑되는 테이블에 @JoinColumn어노테이션의 name으로 설정한 값이 foreign key로 추가되는 것도 볼 수
있습니다. 어떤 테이블에 컬럼이 추가되는지 헷갈릴 수 있는데 @JoinColumn어노테이션을 사용하는 엔티티에 컬럼이 추가된다고 생각하시면 됩니다.
다대일/일대다 양방향 매핑하기
양방향 매핑이란 단방향 매핑이2개 있다고 생각하면 됩니다.
Order
package com.shop.entity;
import com.shop.constant.OrderStatus;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {
@Id
@GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
private LocalDateTime orderDate;//주문일
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus; //주문 상태
private LocalDateTime regTime;
private LocalDateTime updateTime;
}
- 정렬할 때 사용하는 "order" 키워드가 있기 때문에 Order 엔티티에 매핑되는 테이블로 "Orders" 를 지정합니다.
- 한 명의 회원은 여러 번 주문을 할 수 있으므로 주문 엔티티 기준에서 다대일 단방향 매핑을 합니다.
주문 상품 엔티티는 장바구니 상품 엔티티(Cart Item)와 거의 비슷합니다. 주문 상품 엔티티와 주문 엔티티의 단방향 매핑을 먼저 설정하겠습니다.
OrderItem
package com.shop.entity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Getter
@Setter
public class OrderItem {
@Id
@GeneratedValue
@Column(name = "order_item_id")
private Long id;
@ManyToOne
@JoinColumn(name = "item_id")
private Item item;
@ManyToOne
@JoinColumn(name = "order_id")
private Order order;
private int orderPrice;//주문가격
private int count;// 수량
private LocalDateTime regTime;
private LocalDateTime updateTime;
}
- 하나의 상품은 여러 주문 상품으로 들어갈 수 있으므로 주문 상품 기준으로 다대일 단방향 매핑을 설정합니다.
- 한 번의 주문에 여러 개의 상품을 주문할 수 있으므로 주문 상품 엔티티와 주문 엔티티를 다대일 단방향 매핑을 먼저 설정합니다.
다대일과 일대다는 반대 관계라고 생각하시면 됩니다. 주문 상품 엔티티 기준에서 다대일 매핑이었으므로 주문 엔티티 기준에서는 주문 상품 엔티티와 일대다 관계로 매핑하면 됩니다. 또한 양방향 맾핑에서는 '연관 관계 주인'을 설정해야 한다는 점이 중요합니다.
ORDERS와 ORDER_ITEM 테이블을 ORDER_ID를 외래키로 조인하면 주문에 속한 상품이 어떤상품들이 있는지 알 수 있고, 주문 상품은 어떤 주문에 속하는지를 알 수 있습니다. 즉, 테이블은 외래키 하나로 양방향 조회가 가능합니다.
엔티티는 테이블과 다릅니다. 엔티티를 양방향 연관 관계로 설정하면 객체의 참조는 둘인데 외래키는 하나이므로 둘 중 누가 외래키를 관리할지 정해야 합니다
- 연관 관계의 주인은 외래키가 있는 곳으로 설정
- 연관 관계의 주인이 외래키를 관리(등록,수정,삭제)
- 주인이 아닌 쪽은 연관 관계 매핑 시 mappedBy 속성의 값으로 연관 관계의 주인을 설정
- 주인이 아닌 쪽은 읽기만 가능
다음 코드를 통해서 위의 내용을 적용해봅시다. 연관 관계의 주인 설정을 자세히 보셔야 합니다.
Order 엔티티에 OrderItem과 연관 관계 매핑을 추가하겠습니다. OrderItem 엔티티에서 이미 다대일 단방향 매핑을 했으므로 양방향 매핑이 됩니다.
Order
package com.shop.entity;
import com.shop.constant.OrderStatus;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {
@Id
@GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
private LocalDateTime orderDate;//주문일
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus; //주문 상태
@OneToMany(mappedBy = "order")//<추가
private List<OrderItem> orderItems = new ArrayList<>();//< 추가
private LocalDateTime regTime;
private LocalDateTime updateTime;
}
- 주문 상품 엔티티와 일대다 매핑을 합니다. 외래키(order_id)가 order_item 테이블에 있으므로 연관 관계의 주인은 OrderItem 엔티티입니다.Order 엔티티가 주인이 아니므로 "mappedBy" 속성으로 연관 관계의 주인을 설정합니다. 속성의 값으로 "order"를 적어준 이유는 OrderItem에 있는 Order에 의해 관리된다는 의미로 해석하시면 됩니다. 즉, 연고나관계의 주인의 필드인 order를 mappedBy의 값으로 세팅하면 됩니다.
- 하나의 주문이 여러 개의 주문 상품을 갖으므로 List 자료형을 사용해서 매핑을 합니다.
무조건 양방향으로 연관 관계를 매핑하면 해당 엔티티는 엄청나게 많은 테이블과 연관 관계를 맺게되고 엔티티 클래스 자체가 복잡해지기 때문에 연관 관계 단방향 매핑으로 설계 후 나중에 필요할 경우 양방향 매핑을 추가하는 것을 권합니다.
다대다 매핑하기
책에서는 다대다 매핑은 실무에서 사용하지 않는 매핑 관계라고 합니다. 관계형 데이터베이스는 정규화된 테이블 2개로 다대다를 표현할 수 없습니다. 따라서 연결 테이블을 생성해서 다대다 관계를 일대다, 다대다 관계로 풀어냅니다.
다대다 매핑을 사용하지 않는 이유는 연결 테이블에는 컬럼을 추가할 수 없기 때문입니다. 연결 테이블에는 조인 컬럼뿐 아니라 추가 컬럼들이 필요한 경우가 많습니다. 또한 엔티티를 조회할 때 member 엔티티에서 item을 조회하면 중간 테이블이 있기 때문에 어떤 쿼리문이 실행될지 예측하기 도 쉽지 않습니다. 따라서 연결 테이블용 엔티티를 하나 생성한 후 일대다 다대일 관계로 매핑을 하면 됩니다.
'Spring > Spring JPA' 카테고리의 다른 글
Auditing을 이용한 엔티티 공통 속성 공통화 (0) | 2023.03.06 |
---|---|
영속성 전이 (0) | 2023.03.06 |
@Query (0) | 2023.03.02 |
엔티티 매핑 관련 어노테이션 (0) | 2023.02.28 |
JPA (0) | 2023.02.28 |