Book/스프링부트 핵심가이드

연관관계 매핑(of. 다대일(N:1), 일대다(1:N) 매핑)

블로그 주인장 2023. 11. 14.

연관관계 매핑

RDBMS를 사용할 때는 테이블 하나만 사용해서 애플리케이션의 모든 기능을 구현하는 것은 어렵다.

JPA를 사용하는 애플리케이션에서도 테이블의 연관관계를 엔티티 간의 연관관계로 표현이 가능하다.

그 중에서 대일(N:1) , 일대다(1:N) 매핑 방식에 대해 알아보겠습니다.


연관관계 매핑 종류와 방향

  • One To Many : 일대다(1:N)
  • Many To One : 다대일(N:1)

상품 테이블과 공급업체 테이블의 관계

  • 상품 테이블 입장에서 볼 경우 다대일 관계
  • 공급 업체 테이블 입장에서 볼 경우 일대다 관계

 

다대일 단방향 매핑


공급업체 엔티티 클래스

@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Table(name = "provider")
public class Provider extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
}
  • 공급업체는 Provider 라는 도메인으로 정의
  • 간단하게 id와 name만 작성하고, BaseEntity를 이용하여 생성날짜와 변경일자를 상속받는다.

 

상품 엔티티 다대일 연관관계 설정

  • 공급업체 엔티티에 대한 다대일 연관관계를 설정한다.
  • 일반적으로 외래키를 갖는 쪽이 주인 역할을 수행하므로 상품 엔티티가 공급업체 엔티티의 주인이다.(상품 > 공급업체)
public class Product extends BaseEntity{

    //.. 중략...
    
    @OneToOne(mappedBy = "product")
    @ToString.Exclude
    private ProductDetail productDetail;

    @ManyToOne
    @JoinColumn(name = "provider_id")
    @ToString.Exclude
    private Provider provider;
}

 

리포지토리 생성

  • 생성한 공급업체 엔티티를 활용할 수 있도록 리포지토리를 생성한다.
@Repository
public interface ProviderRepository extends JpaRepository<Provider, Long> {
    
}

 

다대일 단방향 연관관계 테스트 코드

@SpringBootTest
class ProviderRepositoryTest {

    @Autowired
    ProviderRepository providerRepository;

    @Autowired
    ProductRepository productRepository;

    @Test
    void relationshipTest1() {
        Provider provider = new Provider();
        provider.setName("ㅇㅇ 물산");

        providerRepository.save(provider);

        Product product = new Product();
        product.setName("가위");
        product.setPrice(1000);
        product.setStock(100);
        product.setProvider(provider);

        productRepository.save(product);

        //테스트
        System.out.println("product: " + productRepository.findById(1L)
                .orElseThrow(RuntimeException::new));

        System.out.println("provider: " + productRepository.findById(1L)
                .orElseThrow(RuntimeException::new).getProvider());

    }
}
  • 연관관계를 테스트하기 위해서 리포지토리에 대해 의존성을 주입받는다.
  • 각 리포지토리를 통해 테스트 데이터를 생성한다.
  • provider 객체를 product에 추가하여 데이터 베이스에 저장한다.

 

Provider 객체를 추가한 Product 객체 저장 쿼리

Hibernate: 
    insert 
    into
        product
        (created_at, updated_at, name, price, provider_id, stock) 
    values
        (?, ?, ?, ?, ?, ?)
  • 쿼리로 데이터를 저장할 때 provider_id 값만 들어가는 것을 볼 수 있다.
  • @JoinColumn에 설정한 이름을 기반으로 자동으로 값을 선정하여 추가하게 된다.

 

다대일 양방향 매핑


공급업체를 통해 등록된 상품을 조회하기 위한 일대다 연관관계를 설정

 

공급업체 엔티티와 상품 엔티티의 일대다 연관관계 설정

@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Table(name = "provider")
public class Provider extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "provider", fetch = FetchType.EAGER)
    @ToString.Exclude
    private List<Product> productList = new ArrayList<>();
}
  • 일대다 연관관계의 경우 여러 상품의 엔티티가 포함될 수 있기 때문에 컬렉션(Collection, List, Map) 형식으로 생성
  • @OneToMany가 붙은 쪽에서 @JoinColumn 어노테이션을 사용하면 상대 엔티티에 외래키가 설정된다.
  • 롬복의 ToString에 의해 순환참조가 발생할 수 있어서, 제외 처리(.Exclude) 진행
  • 'fetch = FetchType.EAGER'는 기본 전략이 Lazy 이기 때문에 즉시 로딩으로 조정한 것이다.
  • mappedBy를 통해 Provider 엔티티의 클래스를 수정하더라도 컬럼은 변경되지 않게 된다.

 

다대일 양방향 연관관계 테스트 코드

@Test
void relationshipTest2() {
    Provider provider = new Provider();
    provider.setName("ㅇㅇ 물산");

    providerRepository.save(provider);

    Product product1 = new Product();
    product1.setName("연필");
    product1.setPrice(2000);
    product1.setStock(100);
    product1.setProvider(provider);

    Product product2 = new Product();
    product2.setName("가위");
    product2.setPrice(5000);
    product2.setStock(500);
    product2.setProvider(provider);

    Product product3 = new Product();
    product3.setName("가방");
    product3.setPrice(20000);
    product3.setStock(2000);
    product3.setProvider(provider);

    productRepository.save(product1);
    productRepository.save(product2);
    productRepository.save(product3);

    List<Product> productList
            = providerRepository.findById(provider.getId()).get().getProductList();

    for (Product product : productList) {
        System.out.println("product = " + product);
    }
}
  • Provider 엔티티 클래스는 Product 엔티티와의 연관관계에서 주인이 아니기 때문에 외래키를 관리할 수 없다.
  • 테스트 데이터를 생성하는 Provider를 등록한 후에 각 Product에 객체를 설정하는 작업을 통해 DB에 저장한다.
  • Product 엔티티를 추가하는 방식으로 데이터베이스에 레코드를 저장하면 연관관계 상 주인이 아니기 때문에 해당 데이터는 데이터베이스에 반영되지 않는다.
provider.getProductList().add(product1);	//무시
provider.getProductList().add(product2);	//무시
provider.getProductList().add(product3);	//무시

 

일대다 단방향 매핑


상품분류 엔티티 클래스

public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String code;

    private String name;
    
    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "category_id")
    private List<Product> productList = new ArrayList<>();
}
  • 상품 분류 엔티티에서 @OneToMany와 @JoinColumn을 사용하면 별도의 설정을 하지 않아도 일대다 단방향 연관관계가 매핑된다.
  • @JoinColumn 어노테이션은 필수 사항은 아니다.
  • 일대다 단방향 관계의 단점은 매핑의 주체가 아닌 반대 테이블에 외래키가 추가된다는 점이다.
    • 다대일 구조와 다르게 외래키를 설정하기 위해 다른 테이블에 대한 update 쿼리를 발생시킨다.

 

리포지토리 생성

  • 생성한 상품 분류 엔티티를 활용할 수 있도록 리포지토리를 생성한다.
@Repository
public interface CategoryRepository extends JpaRepository<Category, Long> {
    
}

 

 

일대다 단방향 연관관계 테스트 코드

 @Test
 void relationshipTest() {

     Product product = new Product();
     product.setName("연필");
     product.setPrice(2000);
     product.setStock(100);

     productRepository.save(product);

     Category category = new Category();
     category.setCode("S1");
     category.setName("도서");
     category.getProductList().add(product);

     categoryRepository.save(category);

     //테스트
     List<Product> list = categoryRepository.findById(1L).get().getProductList();

     for (Product product1 : list) {
         System.out.println("product1 = " + product1);
     }
}
반응형

댓글