728x90
반응형
연관관계 매핑
연관관계 매핑 종류와 방향
연관관계를 맺는 두 엔티티 간에 생성할 수 있는 연관관계의 종류
- One To One : 일대일
- One To Many : 일대다
- Many To One: 다대일
- Many To Many: 다대다
- 공급 업체 입장에서 보면 한 가게에 납품하는 상품이 여러 개 있을 수 있으므로 상품 엔티티와는 일대다 관계
- 상품 입장에서 보면 하나의 공급업체에 속하게 되므로 다대일 관계
- 어떤 엔티티를 중심으로 연관 엔티티를 보느냐에 따라 연관관계의 상태가 달라짐
- 데이터베이스에서는 두 테이블의 연관관계를 설정하면 "외래키"를 통해 서로 조인해서 참조하는 구조로 생성
- JPA를 사용하는 객체지향 모델에서는 엔티티 간 참조 방향을 설정
- 데이터베이스와 관계를 일치시키기 위해 양방향으로 설정해도 무관하지만 비즈니스 로직의 관점에서 봤을 때 는 단방향 관계만 설정해도 해결되는 경우가 많음
- 단방향: 두 엔티티의 관계에서 한쪽의 엔티티만 참조하는 형식
- 양방향: 두 엔티티의 관계에서 각 엔티티가 서로의 엔티티를 참조하는 형식
- 연관관계가 설정되면 한 테이블에서 다른 테이블의 기본값을 외래키
- 일반적으로 외래키를 가진 테이블이 그 관계의 주인 (Owner)이 됨
- 주인은 외래키를 사용할 수 있으나 상대 엔티티는 읽는 작업만 수행
프로젝트 생성
• groupld: com,springboot
• artifactid: relationship
• name: relationship
• Developer Tools: Lombok, Spring Configuration Processor
• Web: Spring Web
• SQL: Spring Data JPA. MariaDB Driver
이전에 사용한 소스코드를 다음과 같이 그대 로 가져와 사용합니다.
(이에 따라 queryDSL의 의존 성과 플러그인 설정을 Don. xml 파일에 그대로 추가해야함)
일대일 매핑
- Product 엔티티를 대상으로 상품 정보 테이블을 일대일 매핑하는 예제
- 하나의 상품에 하나의 상품정보만 매핑되는 일대일 관계
일대일 단방향 매핑
- entity 패키지 안에 상품 정보 ProductDetail 엔티티를 작성
@Entity
@Table(name = "product_detail")
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class ProductDetail extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String description;
// 주목
@OneToOne
@JoinColumn(name = "product_number")
private Product product;
}
- @OneToOne 어노테이션: 다른 엔티티 객체를 필드로 정의했을 때 일대일 연관관계로 매핑하기 위해 사용
- @JoinColum 어노테이션: 매핑할 외래키를 설
- @JoinColum 어노테이션은 기본값이 설정되어 있어 자동으로 이름을 매핑,
그러나 의도한 이름이 들어가지 않기 때문에 name 속성을 사용해 원하는 칼럼명을 지정하는 것이 좋음 - 간단하게 @JoinColum 어노테이션에서 사용할 수 있는 속성들
- name: 매핑할 외래키의 이름을 설정합니다.
- referencedColumnNane: 외래키가 참조할 상대 테이블의 칼럼명을 지정합니다.
- foreignkey: 외래키를 생성하면서 지정할 제약조건을 설정합니다(unique. nul lable, insertable. updatable 등).
- 생성된 상품정보 엔티티 객체들을 사용하기 위해 리포지토리 인터페이스를 생성
// data/repository/ProductDetailRepository.java
public interface ProductDetailRepository extends JpaRepository<ProductDetail, Long> {
}
- 연관 관계를 활용한 데이터 생성 및 조회 기능 테스트 코드
// test/com.springboot.relationship/data/repository/ProductDetailRepositoryTest.java
package com.springboot.relationship.data.repository;
import com.springboot.relationship.data.entity.Product;
import com.springboot.relationship.data.entity.ProductDetail;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class ProductDetailRepositoryTest {
// 상품과 상품 정보에 매핑된 리포지토리 의존성 주입
@Autowired
ProductDetailRepository productDetailRepository;
@Autowired
ProductRepository productRepository;
@Test
public void saveAndReadTest1(){
Product product = new Product();
product.setName("스프링부트 JPA");
product.setPrice(5000);
product.setStock(500);
productRepository.save(product);
ProductDetail productDetail = new ProductDetail();
productDetail.setProduct(product);
productDetail.setDescription("스프링 부트와 JPA를 함께 볼 수 있는 책");
productDetailRepository.save(productDetail);
// 생성한 데이터 조회
System.out.println("savedProduct: " + productDetailRepository.findById(
productDetail.getId()).get().getProduct());
System.out.println("savedProductDetail: " + productDetailRepository.findById(
productDetail.getId()).get());
}
}
- 상품과 상품정보에 매핑된 리포지토리에 대해 의존성 주입
- ProductDeatil 객체에서 Product 객체를 일대일 단방향 연관관계를 설정했기 때문에 ProductDetailRepository에서 ProductDetail 객체를 조회한 후 연관관계 매핑된 Product 객체를 조회
- 테스트를 실행하면 하이버네이트로 결과 확인
- select 구문을 보면 ProductDetail 객체와 Product 객체가 함께 조회됨
- 이처럼 엔티티를 조회할 때 연관된 엔티티도 함께 조회하는 것을 '즉시 로딩'이라 함
- @OneToOne 어노테이션 인터페이스
public @interface OneToOne {
Class targetEntity() default void.class;
CascadeType[] cascade() default {};
FetchType fetch() default FetchType.EAGER;
boolean optional() default true;
String mappedBy() default "";
boolean orphanRemoval() default false;
}
- @ OneToOne 어노테이션은 기본 fetch 전략으로 EAGER(즉시 로딩) 전략이 채택
- optional() 메서드는 기본값으로 true가 설정
- 기본값이 true인 상태는 매핑되는 값이 nullable이라는 것을 의미
- @OneToOne 어노테이션에 'optional=false' 설정을 하면 product가 null인 값을 허용하지 않음
- 코드 변경 후 테스트 코드를 실해앟면 left outer join이 inner join으로 바뀌어 실행되는 것을 확인함
- 객체에 대한 설정에 따라 JPA는 최적의 쿼리를 생성해서 실행할 수 있음
일대일 양방향 매핑
- 이번에는 앞에서 생성한 일대일 단방향 설정을 양방향 설정으로 변경
- 객체에서의 양 방향 개념은 양쪽에서 단방향으로 서로를 매핑하는 것을 의미
- Product 엔티티에 일대일 양방향 매핑을 위해서는 다음과 같이 추가
@Entity
// 생략..
public class Product extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long number;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer price;
@Column(nullable = false)
private Integer stock;
// 추가
@OneToOne
private ProductDetail productDetail;
}
- 코드를 실행하면 product 테이블에도 칼럼이 생성됨
- 테스트 코드를 실행하면 양쪽에서 외래키를 가지고 left outer join이 두 번이나 수행되어 효율성이 떨어지는 것을 확인할 수 있음
- 실제 데이터베이스에서도 테이블 간 연관관계를 맺으면 한쪽 테이블이 외래키를 가지는 구조
- 앞에서 언급한 '주인' 개념을 사용함
- JPA에서도 데이터베이스의 연관관계를 반영해서 한 쪽의 테이블에서만 외래키를 바꿀 수 있도록 정하는 것이 좋음
- 이 경우 엔티티는 양방향으로 매핑하되 한쪽에게만 외래키를 줌
- mappedBy : 어떤 객체가 주인인지 표시하는 속성
@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Table(name="product")
public class Product extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long number;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer price;
@Column(nullable = false)
private Integer stock;
// 주목
@OneToOne(mappedBy = "product")
private ProductDetail productDetail;
}
- mappedBy에 들어가는 값은 연관관계를 갖고 있는 상대 엔티티에 있는 연관관계 필드의 이름
- ProductDetail 엔티티가 Product 엔티티의 주인이 됨
- 테스트 코드를 실행하면 toString 실행 시점에서 StackOverflowError 발생
- 양방향으로 연관관계가 설정되면 ToString을 사용할 때 순환 참조가 발생하기 때문
- 필요한 경우가 아니라면 대체로 단방향으로 연관관계를 설정하거나 양방향 설정이 필요할 경우에는 순환참조 제거를 위해 exclude를 사용해 ToString에서 제외 설정해야함
// data/entity/Product.java
@OneToOne(mappedBy = "product")
@ToString.Exclude
private ProductDetail productDetail;
다대일, 일대다 매핑
- 상품 테이블과 공급업체 테이블은 상품 테이블의 입장에서 볼 경우에는 다대일 관계
- 공급업체 테이블의 입장에서 볼 경우에는 일대다 관계
다대일 단방향 매핑
- 공급업체 테이블에 매핑되는 Provider 엔티티 클래스 생성
@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;
}
- BaseEntity를 통해 생성일자와 변경일자를 상속받음
- 상품 엔티티에 공급 업체 번호를 받기 위한 엔티티 필드 구성 추가
package com.springboot.relationship.data.entity;
// 생략..
public class Product extends BaseEntity {
// 생략..
@ManyToOne
@JoinColumn(name = "provider_id")
@ToString.Exclude
private Provider provider;
}
- 일반적으로 외래키를 갖는 쪽이 주인의 역할을 수행
- 이 경우 상품 엔티티가 공급업체 엔티티의 주인입니다.
- ProviderRepository를 생성
public interface ProviderRepository extends JpaRepository<Provider, Long> {
}
- ProductRepository를 활용한 테스트
package com.springboot.relationship.data.repository;
import com.springboot.relationship.data.entity.Product;
import com.springboot.relationship.data.entity.Provider;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class ProviderRepositoryTest {
// 의존성 주입
@Autowired
ProviderRepository providerRepository;
@Autowired
ProductRepository productRepository;
@Test
void relationshipTest1(){
// 테스트 데이터 생산
Provider provider = new Provider();
provider.setName("Corner");
providerRepository.save(provider);
Product product = new Product();
product.setName("가위");
product.setPrice(5000);
product.setStock(500);
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_id 값만 들어감
- product 테이블에 @JoinColum에 설정한 이름을 기반으로 자동으로 값을 선정해서 추가
- Product 엔티티에서 단방향으로 Provider 엔티티 연관관계를 맺고 있기 때문에 ProductRepository만으로 Provider 객체도 조회가 가능
다대일 양방향 매핑
- 공급업체를 통해 등록된 상품을 조회하기 위한 일대다 연관관계를 설정
- JPA에서는 이처럼 양쪽에서 단방향으로 매핑하는 것이 양방향 매핑 방식
- 이번에는 공급업체 엔티티에서만 연관관계를 설정
@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;
// @OneToMny의 기본 fetch 전략이 Lazy이기 때문에 즉시 로딩으로 조정
@OneToMany(mappedBy = "provider", fetch = FetchType.EAGER)
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
}
- 일대다 연관관계의 경우 여러 상품 엔티티가 포함될 수 있어 컬렉션(collection, List, Map) 형식으로 필드를 생성
- @OneToMany가 붙은 쪽에서 @JoinColum 어노테이션을 사용하면 상대 엔티티에 외래키가 설정
- 롬복의 ToString에 의해 순환참조가 발생할 수 있어 Tostring에서 제외 처리를 하는 것이 좋음
- 공급업체 엔티티로 연관된 엔티팅릐 값을 가져올 수 있는지 테스트 코들르 작성해 테스트
@SpringBootTest
public class ProviderRepositoryTest {
@Autowired
ProviderRepository providerRepository;
@Autowired
ProductRepository productRepository;
@Test
void relationshipTest(){
// 테스트 데이터 생성
Provider provider = new Provider();
provider.setName("Corner");
providerRepository.save(provider);
Product product1 = new Product();
product1.setName("펜");
product1.setPrice(1000);
product1.setStock(1000);
product1.setProvider(provider);
Product product2 = new Product();
product2.setName("가방");
product2.setPrice(5000);
product2.setStock(300);
product2.setProvider(provider);
Product product3 = new Product();
product3.setName("모자");
product3.setPrice(500);
product3.setStock(50);
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);
}
}
}
일대다 단방향 매핑
- 상품 분류 테이블 생성
- 상품 분류의 도메인 이름은 Category로 설정
먼저 상품 분류 엔티티와 레포지토리를 생성합니다.
// data/entity/Category.java
@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString
@EqualsAndHashCode
@Table(name="category")
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> products = new ArrayList<>();
}
- 코드를 실행하면 상품 분류 테이블이 생성되고 상품 테이블 외래키가 추가되는 것을 확인
- @OneToMany와 @JoinColum을 사용하면 상품 엔티티에서 별도의 설정 없이 일대다 단방향 연관관계가 매핑
- @JoinColum 어노테이션은 필수 사항은 아님
이 어노테이션을 사용하지 않으면 중간 테이블로 Join 테이블이 생성되는 전략이 채택
- 지금 같은 일대다 단방향 관계의 단점은 매핑의 주체가 아닌 반대 테이블에 외래키가 추가된다는 점
- 이 방식은 다대일 구조와 다르게 외래키를 설정하기 위해 다른 테이블에 대한 update 쿼리를 발생
- 이를 확인하기 위해 저장할 레포지토리와 테스트 코드 작성
// data/repository/CategoryRepository.java
public interface CategoryRepository extends JpaRepository<Category, Long> {
}
// test/com.springboot.relationship/data/repository/CategoryRepositoryTest.java
@SpringBootTest
public class CategoryRepositoryTest {
@Autowired
ProductRepository productRepository;
@Autowired
CategoryRepository categoryRepository;
@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.getProducts().add(product);
categoryRepository.save(category);
// 테스트 코드
List<Product> productList = categoryRepository.findById(1L).get().getProducts();
for(Product foundProduct:productList){
System.out.println(product);
}
}
}
- 테스트 코드를 실행시키면 일대다 연관관계에서는 연관관계 설정을 위한 update 쿼리가 발생
- 같은 문제를 해결하기 위해는 다대일 연관관계를 사용하는 것이 좋음
- 일대다 연관관계에서는 category와 product의 조인이 발생해서 상품 데이터를 정상적으로 가져옴
다대다 매핑
- 다대다(M:N) 연관관계는 실무에서 거의 사용되지 않는 구성
- 다대다 연관관계를 상품과 생산업체의 예로 들자면 한 종류의 상품이 여러 생산업체를 통해 생산될 수 있고, 생산업체 한 곳이 여러 상품을 생산할 수도 있음
- 다대다 연관관계에서는 각 엔티티에서 서로를 리스트로 가지는 구조
- 이런 경우에는 교차 엔티티라고 부르는 중간 테이블을 생성해서 다대다 관계를 일대다 또는 다대일 관계로 해소
@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Table(name="producer")
public class Producer extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
private String name;
// 주목
@ManyToMany
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
public void addProduct(Product product){
productList.add(product);
}
}
- 다대다 연관관계는 @ManyToMany 어노테이션으로 설정
- 리스트로 필드를 가지는 객체에서는 외래키를 가지지 않기 때문에 별도의 @Joincolum은 설정하지 않아도 됨
- 엔티티를 생성하고 애플리케이션을 실행하면 생산업체 테이블이 생성되고 별도의 외래키가 추가되지 않음
- 중간 테이블을 별도의 이름으로 설정하고 싶다면 @ManyToMany 어노테이션 아래 @JoinTable(name = "이름")으로 어노테이션을 정의하면 됨
- 리포지토리를 통해 생산업체에 대한 기본적인 데이터베이스 조작이 가능함
public interface ProducerRepository extends JpaRepository<Producer, Long> {
}
public class ProducerRepositoryTest {
@Autowired
ProducerRepository producerRepository;
@Autowired
ProductRepository productRepository;
@Test
@Transactional
void relationshipTest(){
Product product1 = saveProduct("동글펜", 400, 1000);
Product product2 = saveProduct("네모 공책", 200, 200);
Product product3 = saveProduct("지우개", 300, 1400);
Producer producer1 = saveProducer("flature");
Producer producer2 = saveProducer("wikibooks");
producer1.addProduct(product1);
producer1.addProduct(product2);
producer2.addProduct(product2);
producer2.addProduct(product3);
producerRepository.saveAll(Lists.newArrayList(producer1,producer2));
System.out.println(producerRepository.findById(1L).get().getProductList());
}
private Product saveProduct(String name, Integer price, Integer stock){
Product product = new Product();
product.setName(name);
product.setPrice(price);
product.setStock(stock);
return productRepository.save(product);
}
private Producer saveProducer(String name){
Producer producer = new Producer();
producer.setName(name);
return productRepository.save(producer);
}
}
- 가독성을 위해 리포지토리를 통해 테스트 데이터를 생성하는 부분을 별도 메서드로 구현
- 리포지토리를 사용하게 되면 매번 트랜잭션이 끊어져 생산업체 엔티티에서 상품 리스트를 가져오는 작업이 불가능
- 이 문제를 해소하기 위해 테스트 메서드에 @Transactional 어노테이션을 지정해 트랜잭션이 유지되도록 구성해서 테스트를 진행
- 연관관계를 설정했기 때문에 정상적으로 생산업체 엔티티에서 상품 리스트를 가져옴
- 앞의 테스트를 통해 테스트 데이터를 생성하면 product 테이블과 producer 테이블에 레코드가 추가되지만 보여지는 내용만으로는 연관관계 설정 여부를 확인하기 어려움
- 그 이유는 다대다 연관관계 설정 을 통해 생성된 중간 테이블에 연관관계 매핑이 돼 있기 때문
- 중간 테이블을 확인하면 앞서 설정한 연관관계에 맞춰 양 테이블의 기본키를 매핑한 레코드가 생성된 것을 볼 수 있음
다대다 양방향 매핑
@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@Table(name="product")
public class Product extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long number;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer price;
@Column(nullable = false)
private Integer stock;
@OneToOne(mappedBy = "product")
@ToString.Exclude
private ProductDetail productDetail;
@ManyToOne
@JoinColumn(name = "provider_id")
@ToString.Exclude
private Provider provider;
// 생산업체에 대한 다대다 연관관계 설정
@ManyToMany
@ToString.Exclude
private List<Producer> producers = new ArrayList<>();
public void addProducer(Producer producer){
this.producers.add(producer);
}
}
- 필요에 따라 mappedBy 속성을 사용해 두 엔티티 간 연관관계의 주인을 설정
- 중간 테이블이 연관관계를 설정하고 있기 때문에 데이터베이스의 테이블 구조는 변경되지 않음
- 테스트 코드 작성
Public class ProducerRepositoryTest {
// 생략..
@Test
@Transactional
void relationshipTest2(){
Product product1 = saveProduct("동글펜", 400, 1000);
Product product2 = saveProduct("네모 공책", 200, 200);
Product product3 = saveProduct("지우개", 300, 1400);
// 생략..
// 양방향 연관관계 설정
product1.addProducer(producer1);
product2.addProducer(producer2);
product2.addProducer(producer1);
product3.addProducer(producer2);
producerRepository.saveAll(Lists.newArrayList(producer1,producer2));
productRepository.saveAll(Lists.newArrayList(product1, product2, product3));
System.out.println(producerRepository.findById(1L).get().getProductList());
System.out.println(productRepository.findById(2L).get().getProducers());
}
// 생략..
}
- 양방향 연관관계 설정을 위해 연관관계 설정 코드를 추가
- 연관관계를 설정하고 각 엔티티에 연관된 엔티티를 출력하면 정상적으로 출력됨
- 이렇게 다대다 연관관계를 설정하면 중간 테이블을 통해 연관된 엔티티의 값을 가져올 수 있음
- 다만 다대다 연관관계에서는 관리하기 힘든 중간 테이블이 생성되기 때문에 예기치 못한 쿼리가 생길 수 있음
- 이러한 다대다 연관관계의 한계를 극복하기 위해서는 중간 테이블을 생성하는 대신 일대다 다대일로 연관관계를 맺을 수 있는 중간 엔티티로 승격시켜 JPA에서 관리할 수 있게 생성하는 것이 좋음
영속성 전이
- 영속성 전이(cascade)란 특정 엔티티의 영속성 상태를 변경할 때 그 엔티티와 연관된 엔티티의 영속성에도 영향을 미쳐 영속성 상태를 변경하는 것
- 연관관계와 관련된 어노테이션을 보면 위와 같이 cascade()이라는 은 영속성 전이를 설정하는 데 활용되는 요소 확인
- cascade() 요소와 함께 사용하는 영속성 전이 타입
종류 | 설명 |
ALL | 모든 영속 상태 변경에 대해 영속성 전이를 적용 |
PERSIST | 엔티티가 영속화할 때 연관된 엔티티도 함께 영속화 |
MERGE | 엔티티를 영속성 컨텍스트에 병합할 때 연관된 엔티티도 병합 |
REMOVE | 엔티티를 제거할 때 연관된 엔티티도 제거 |
REFRESH | 엔티티를 새로고침할 때 연관된 엔티티도 새로고침 |
DETACH | 엔티티를 영속성 컨텍스트에서 제외하면 연관된 엔티티도 제외 |
- 영속성 전이에 사용되는 타입은 엔티티 생명주기와 연관이 있음
- 한 엔티티가 cascade 요소의 값으로 주어진 영속 상태의 변경이 일어나면 매핑으로 연관된 엔티티에도 동일한 동작이 일어나도록 전이를 발생
- cascade() 요소의 리턴 타입은 배열 형식
- 개발자가 사용하고자 하는 cascade 타입을 골라 각 상황에 적용
영속성 전이 적용
- 상품 엔티티와 공급업체 엔티티를 사용해 영속성 전이를 적용
- 한 가게가 새로운 공급업체와 계약하며 몇 가지 새 상품을 입고시키는 상황에 어떻게 영속성 전이가 적용되는지 살펴보기
- 엔티티를 데이터베이스에 추가하는 경우로 영속성 전이 타입으로 PERSIST를 지정
@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 어노테이션
@OneToMany(mappedBy = "provider", cascade = CascadeType.PERSIST)
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
}
- 테스트 코드 작성
@SpringBootTest
public class ProviderRepositoryTest {
@Autowired
ProviderRepository providerRepository;
@Autowired
ProducerRepositoryTest productRepository;
@Test
void cascadeTest(){
Provider provider = saveProvider("new provider");
Product product1 = saveProduct("동글펜", 400, 1000);
Product product2 = saveProduct("네모 공책", 200, 200);
Product product3 = saveProduct("지우개", 300, 1400);
// 연관관계 설정
product1.setProvider(provider);
product2.setProvider(provider);
product3.setProvider(provider);
provider.getProductList().addAll(Lists.newArrayList(product1, product2, product3));
// 영속성 전이 수행
providerRepository.save(provider);
}
private Product saveProduct(String name, Integer price, Integer stock){
Product product = new Product();
product.setName(name);
product.setPrice(price);
product.setStock(stock);
return product;
}
private Provider saveProvider(String name){
Provider provider = new Provider();
provider.setName(name);
return provider;
}
}
- 영속성 전이를 테스트하기 위해 객체에는 영속화 작업을 수행하지 않고, 연관 관계만 설정
- 영속성 전이가 수행되는 부분은 주석으로 설정
providerRepository.save(provider);
- 지금까지는 엔티티를 데이터베이스에 저장하기 위해 각 엔티티를 저장하는 코드를 작성해야 함
- 영속성 전이를 사용하면 부모 엔티티가 되는 Provider 엔티티만 저장하면 코드에 작성돼 있는 Cascade.PERSIST에 맞춰 상품 엔티티도 함께 저장
- 특정 상황에 맞춰 영속성 전이 타입을 설정하면 영속 상태의 변화에 따라 연관된 엔티티들의 동작도 함께 수행
- 다만 자동 설정으로 동작하는 코드들이 정확히 어떤 영향을 미치는지 파악할 필요가 있음
고아 객체
- JPA에서 고아(orphan)란 부모 엔티티와 연관관계가 끊어진 엔티티
- JPA에는 이러한 고아 객체를 자동으로 제거하는 기능
- 물론 자식 엔티티가 다른 엔티티와 연관관계를 가지고 있다면 이 기능은 사용하지 않는 것이 좋음
- 현재 예제에서 사용되는 상품 엔티티는 다른 엔티티와 연관관계가 많이 설정돼 있지만 그 부분은 예외로 두고 테스트를 진행
- 고아 객체를 제거하는 기능을 사용하기 위해서는 공급업체 엔티티를 다음과 같이 작성
@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;
// 'orphanRemoval = true'은 고아 객체를 제거하는 기능
@OneToMany(mappedBy = "provider", cascade = CascadeType.PERSIST, orphanRemoval = true)
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
}
- 테스트 코드를 작성
@SpringBootTest
public class ProviderRepositoryTest {
// 생략...
product1.setProvider(provider);
product2.setProvider(provider);
product3.setProvider(provider);
provider.getProductList().addAll(Lists.newArrayList(product1, product2, product3));
providerRepository.saveAndFlush(provider);
// 엔티티 출력
providerRepository.findAll().forEach(System.out::println);
productRepository.findAll().forEach(System.out::println);
// 생략...
}
- 먼저 테스트 데이터를 저장한 후, 연관관계 매핑을 수행
- 연관관계가 매핑된 각 엔티티들을 저장한 후 각 엔티티를 출력하면 공급업체 엔티티 1개, 상품 엔티티 3개가 출력
- 고아 객체를 생성하기 위해 공급업체 엔티티를 가져온 후 첫 번째로 매핑돼 있는 상품 엔티티의 연관관계를 제거
- 전체 조회 코드를 수행하면 다음과 같이 연관관계가 끊긴 상품의 엔티티가 제거된 것을 확인
QUIZ
- 연관관계를 맺는 두 엔티티 간에 생성할 수 있는 연관관계의 종류는 일대일, ( 일대다 ) , 다대일, 다대다 총 4종류이다
- 데이터베이스에서는 두 테이블의 연관관계를 설정하면 ( 외래키 )를 통해 서로 조인해서 참조하는 구조로 생성된다
- ( @JoinColum ) 어노테이션은 매핑할 외래키를 설정할 때 사용되며 기본값이 설정되어 있어 자동으로 이름을 매핑해준다
- 양방향으로 연관관계가 설정되면 ToString을 사용할 때 순환 참조( StackOverflowError)가 발생 하기 때문에 순환참조 제거를 위해 ( @ToString.Exclude )를 사용한다
- 일대다 연관관계의 경우 여러 상품 엔티티가 포함될 수 있어 ( 컬렉션 ) (List, Map) 형식으로 필드를 생성
- 일대다 단방향 관계의 단점은 매핑의 주체가 아닌 반대 테이블에 외래키가 추가되어 외래키를 설정하기 위해 다른 테이블에 대한 ( update ) 쿼리를 발생한다.
- ( 영속성 전이 )란 특정 엔티티의 영속성 상태를 변경할 때 그 엔티티와 연관된 엔티티의 영속성에도 영향을 미쳐 영속성 상태를 변경하는 것이다.
- 영속성 전이를 위한 코드를 작성하시오.
@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;
// 영속성 전이 코드 작성
}
9. 상품 엔티티에 공급 업체 번호를 받기 위한 엔티티 필드를 추가하는 코드를 작성하시오.
@Entity
// 생략..
public class Product extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long number;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer price;
@Column(nullable = false)
private Integer stock;
@OneToOne(mappedBy = "product")
@ToString.Exclude
private ProductDetail productDetail;
// 코드 추가
}
8.
@OneToMany(mappedBy = "provider", cascade = CascadeType.PERSIST)
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
9.
@ManyToOne
@JoinColumn(name = "provider_id")
@ToString.Exclude
private Provider provider;
[출처] 장정우, 『스프링 부트 핵심 가이드』, 위키북스(2022), p.159-207.
ⓒ 다라
반응형
'프레임워크 > Spring' 카테고리의 다른 글
서비스의 인증과 권한 부여 (1) | 2023.12.31 |
---|---|
ORM의 개념과 JPA (0) | 2023.10.30 |
JPA와 스프링 데이터 JPA (0) | 2023.10.03 |