be_better

[Springboot] 연관관계 매핑 - 다대일, 일대다 매핑 본문

Springboot/이론

[Springboot] 연관관계 매핑 - 다대일, 일대다 매핑

NiceTry 2023. 11. 17. 21:35

목차

    다대일, 일대다 매핑

    다대일 단방향 매핑

    공급업체 엔티티 클래스 생성 Provider.java
    @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;
    }

    상품 엔티티에서 공급업체의 번호를 받기 위해 다음과 같이 엔티티 필드의 구성을 추가한다.

     

    Product 엔티티에 Provider 필드 선언
    @Entity
    @Getter
    @Setter
    @NoArgsConstructor
    @ToString(callSuper = true)
    @EqualsAndHashCode(callSuper = true)
    @Table(name = "product")
    public class Product extends BaseEntity {
    
        ...(생략)
    
        @ManyToOne
        @JoinColumn(name = "provider_id")
        @ToString.Exclude
        private Provider provider;
    }

     

    db 상태를 확인해보면 다음과 같다.

    일반적으로 외래키를 갖는 쪽이 주인의 역할을 수행하기 때문에 이 경우 상품 엔티티가 공급업체 엔티티의 주인이 된다.

     

    ProviderRepository를 생성하고 Test코드를 작성한다.

    Product, Provider 두 엔티티의 주인은 Product 엔티티이기 때문에 ProductRepository를 활용해 테스트한다.

     

    ProductRepositoryTest.java에 Product, Provider 엔티티 연관관계 테스트코드
    @Test
    void relationshipTest1() {
        //given
        Provider provider = new Provider();
        provider.setName("00물산");
    
        Product product = new Product();
        product.setName("가위");
        product.setPrice(5000);
        product.setStock(500);
        product.setProvider(provider);
    
        //when
        providerRepository.save(provider);
        productRepository.save(product);
    
        //then
        System.out.println(
                "product : " + productRepository.findById(1L)
                        .orElseThrow(RuntimeException::new)
        );
    
        System.out.println(
                "provider : " + productRepository.findById(1L)
                        .orElseThrow(RuntimeException::new).getProvider()
        );
    }

     

    테스트코드를 실행한 결과는 다음과 같다.

    쿼리로 데이터를 저장할 때는 provider_id 값만 들어가는 것을 볼 수 있다. 이렇게 product 테이블에는 @JoinColumn에 설정한 이름을 기반으로 자동으로 값을 선정해서 추가하게 된다.

    Product 엔티티에서 단방향으로 Provider 엔티티 연관관계를 맺고 있기 때문에 ProductRepository만으로도 Provider 객체도 조회가 가능한 것이다.

     

    select 구문 실행 쿼리는 다음과 같다.

     

    다대일 양방향 매핑

    다음은 반대로 공급업체를 통해 등록된 상품을 조회하기 위한 일대다 연관관계에 대한 내용이다.

     

    Provider 엔티티에 Product 필드 일대다 연관관계 설정
    @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 어노테이션을 사용하면 상대 엔티티에 외래키가 설정된다.

    @OneToMany의 fetch 속성값을 'fecth = FetchType.EAGER' 즉, 즉시로딩으로 설정한 것은 @OneToMany의 기본 fetch 전략이 Lazy(지연로딩) 이기 때문에 즉시 로딩으로 조정한 것이다. 테스트 과정을 수행할 때 지연 로딩 방식을 사용하면 'no Session'으로 에러가 발생하기 때문에 조정하는 것이 좋다.

     

    Provider 엔티티 기반의 테스트 코드
    @SpringBootTest
    class ProviderRepositoryTest {
    
        @Autowired
        ProductRepository productRepository;
    
        @Autowired
        ProviderRepository providerRepository;
    
        @Test
        void relationshipTest() {
            // 테스트 데이터 생성
            Provider provider = new Provider();
            provider.setName("00상사");
    
            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(20000);
            product2.setStock(200);
            product2.setProvider(provider);
    
            Product product3 = new Product();
            product3.setName("노트");
            product3.setPrice(3000);
            product3.setStock(1000);
            product3.setProvider(provider);
    
            productRepository.save(product1);
            productRepository.save(product2);
            productRepository.save(product3);
    
            List<Product> products =
                    providerRepository
                            .findById(provider.getId())
                            .get()
                            .getProductList();
    
            for (Product product : products) {
                System.out.println(product);
            }
        }
    
    }

    Provider 엔티티 클래스는 Product 엔티티와의 연관관계에서 주인이 아니기 때문에 외래키를 관리할 수 없다. 그렇기 때문에 Provider를 등록한 후 각 Product에 객체를 설정하는 작업을 통해 데이터베이스에 저장한다.

     

    만약 Provider의 productList 필드에 Product 엔티티를 추가하는 방식으로 데이터베이스에 레코드를 저장하게 되면 Provider 엔티티 클래스는 연관관계의 주인이 아니기 때문에 해당 데이터는 데이터베이스에 반영되지 않는다.

    주인이 아닌 엔티티에서 연관관계를 설정한 예 (잘못된 예시)
    provider.getProductList().add(product1); // 무시
    provider.getProductList().add(product2); // 무시
    provider.getProductList().add(product3); // 무시

     

    일대다 단방향 매핑

    @OneToMany를 사용하는 입장에서는 어느 엔티티 클래스도 연관관계의 주인이 될 수 없기 때문에 일대다 양방향 매핑은 다룰 수 없다. 

     

    실습을 위한 엔티티 상품분류(Category)

     

    상품 분류 엔티티 클래스
    @Entity
    @Getter
    @Setter
    @NoArgsConstructor
    @ToString(callSuper = true)
    @EqualsAndHashCode(callSuper = true)
    @Table(name = "category")
    public class Category extends BaseEntity {
    
        @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> products = new ArrayList<>();
    
    }

     

    데이터베이스 테이블 확인

    상품 분류 엔티티에서 @OneToMany와 @JoinColumne을 사용하면 상품 엔티티에서 별도의 설정을 하지 않아도 일대다 연관관계가 매핑된다. @JoinColumn 어노테이션은 필수 사항은 아니지만 사용하지 않으면 중간 테이블로 Join 테이블이 생성되는 전략이 채택되어서 어노테이션을 사용하는게 좋다.

     

    product 테이블에 category_id가 외래키로 생성된 것이 확인된다면 단방향 매핑이 완료된 상태이다. 이와 같이 일대다 단방향 관계의 단점은 매핑의 주체가 아닌 반대 테이블에 외래키가 추가된다는 점이다.

     

    테스트를 하기 위해 CategoryRepository를 생성하고 테스트 코드를 작성
    @SpringBootTest
    class CategoryRepositoryTest {
    
        @Autowired
        ProductRepository productRepository;
    
        @Autowired
        CategoryRepository categoryRepository;
    
        @Test
        void relationshipTest() {
            // 테스트 데이터 생성
            Product product1 = new Product();
            product1.setName("펜");
            product1.setPrice(2000);
            product1.setStock(100);
    
            Product product2 = new Product();
            product2.setName("가위");
            product2.setPrice(5000);
            product2.setStock(200);
    
            productRepository.save(product1);
            productRepository.save(product2);
    
            Category category = new Category();
            category.setCode("S1");
            category.setName("도서");
            category.getProducts().add(product1);
            category.getProducts().add(product2);
    
            categoryRepository.save(category);
    
            // 테스트 코드
            List<Product> products = categoryRepository.findById(1L).get().getProducts();
    
            for (Product foundProduct : products) {
                System.out.println(foundProduct);
            }
        }
    }

    실행 결과

    일대다 연관관계에서는 위와 같이 연관관계 설정을 위한 update 쿼리가 발생한다. 이 같은 문제를 해결하기 위해서는 일대다 양방향 연관관계를 사용하기보다는 다대일 연관관계를 사용하는 것이 좋다.

     

    출력문 결과를 보면 하나의 카테고리에 Product 엔티티들이 잘 들어가있는 것을 확인할 수 있다.