어떤 엔티티를 중심으로 연관 엔티티를 보느냐에 따라 연관관계의 상태가 달라짐
📌 Product 엔티티를 대상으로 일대일 매핑될 상품 정보 테이블 생성
📌 entity 패키지 안에 상품정보 엔티티 작성
@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;
}
📌 product_detail 테이블 생성
: hibernate.ddl-auto의 값을 create로 설정 후 애플리케이션을 실행하면 하이버네이트에서 자동으로 테이블을 생성
📌 ProductDetailRepository 인터페이스
: 생성된 상품정보 엔티티 객체들을 사용하기 위해 리포지토리 인터페이스 생성
public interface ProductDetailRepository extends JpaRepository<ProductDetail, Long> {
}
📌 ProductRepository와 ProductDetailRepository에 대한 테스트 코드
: 연관관계를 활용한 데이터 생성 및 조회 기능을 테스트 코드로 간략하게 작성
@SpringBootTest
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());
}
}
📌 @OneToOne 어노테이션 인터페이스
public @interface OneToOne {
Class targetEntity() default void.class;
CascadeType[] cascade() default {};
FetchType fetch() default EAGER;
boolean optional() default true;
String mappedBy() default "";
boolean orphanRemoval() default false;
}
📌 Product 객체가 반드시 있어야 하는 ProductDetail 엔티티 클래스
//..생략..
public class ProductDetail extends BaseEntity {
//..생략..
@OneToOne(optional=false) // null값 허용 x
@JoinColumn(name = "product_number")
private Product product;
}
: 양방향은 사실 양쪽에서 단방향으로 서로를 매핑하는 것을 의미
📌 일대일 양방향 매핑을 위한 Product 엔티티
@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
private ProductDetail productDetail;
}
📌 mappedBy 속성을 추가한 Product 엔티티 클래스
//..생략..
public class Product extends BaseEntity {
//..생략..
@OneToOne(mappedBy = "product")
//..생략..
}
📌 @ToString 어노테이션의 대상 제외
public class Product extends BaseEntity {
//..생략..
@OneToOne(mappedBy = "product")
@ToString.Exclude
private ProductDetail productDetail;
}
📌 공급업체 언티티 클래스 생성
@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;
}
📌 상품 엔티티와 공급업체 언티티의 다대일 연관관계 설정
public class Product extends BaseEntity {
//..생략..
@ManyToOne
@JoinColumn(name = "provider_id")
@ToString.Exclude
private Provider provider;
}
📌 ProviderRepository 인터페이스
public interface ProviderRepository extends JpaRepository<Provider, Long> {
}
📌 Product, Provider 엔티티 연관관계 테스트
: Product, Provider 엔티티에서 주인은 Product 엔티티이므로 ProductRepository를 활용해 테스트
@SpringBootTest
class ProviderRepositoryTest {
@Autowired
ProductRepository productRepository;
@Autowired
ProviderRepository providerRepository;
@Test
void relationshipTest1() {
// 테스트 데이터 생성
Provider provider = new Provider();
provider.setName("ㅇㅇ물산");
providerRepository.save(provider);
Product product = new Product();
product.setName("가위");
product.setPrice(5000);
product.setStock(500);
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());
}
}
📌 공급업체 엔티티와 상품 엔티티의 일대다 연관관계 설정
: 기존 상품 엔티티와 공급업체 엔티티 사이의 다대일 단방향 연관관계와 반대인 공급업체를 통해 등록된 상품을 조회하기 위한 일대다 연관관계 설정
public class Provider extends BaseEntity {
//..생략..
@OneToMany(mappedBy = "provider", fetch = FetchType.EAGER)
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
}
📌 Prvider 엔티티 기반의 테스트 코드
@SpringBootTest
class ProviderRepositoryTest {
@Autowired
ProductRepository productRepository;
@Autowired
ProviderRepository providerRepository;
@Test
void relationshipTest() {
// 테스트 데이터 생성
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(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);
System.out.println("check 1");
List<Product> products = providerRepository.findById(provider.getId()).get()
.getProductList();
for (Product product : products) {
System.out.println(product);
}
}
}
provider.getProductList().add(product1); // 무시
provider.getProductList().add(product2); // 무시
provider.getProductList().add(product3); // 무시
📌 상품분류 엔티티 클래스 생성
@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<>();
}
📌 CategoryRepository 인터페이스
public interface CategoryRepository extends JpaRepository<Category, Long> {
}
📌 CategoryRepository를 활용한 테스트
@SpringBootTest
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> products = categoryRepository.findById(1L).get().getProducts();
for(Product foundProduct : products){
System.out.println(foundProduct);
}
}
}
: 다대다 연관관계는 실무에서 거의 사용되지 않는 구성. 다대다 연관관계에서는 각 엔티티에서 서로를 리스트로 가지는 구조가 만들어짐. 이런 경우에는 교차 엔티티라고 부르는 중간 테이블을 생성해서 다대다 관계를 일대다 또는 다대일 관계로 해소함.
📌 생산업체 엔티티 생성
@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> products = new ArrayList<>();
public void addProduct(Product product){
products.add(product);
}
}
📌 ProducerRepository 생성
public interface ProducerRepository extends JpaRepository<Producer, Long> {
}
📌 생산업체 연관관계 테스트
@SpringBootTest
class ProducerRepositoryTest {
@Autowired
ProducerRepository producerRepository;
@Autowired
ProductRepository productRepository;
@Test
@Transactional
void relationshipTest() {
Product product1 = saveProduct("동글펜", 500, 1000);
Product product2 = saveProduct("네모 공책", 100, 2000);
Product product3 = saveProduct("지우개", 152, 1234);
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().getProducts());
}
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 producerRepository.save(producer);
}
}
📌 상품 엔티티에서 생산업체 엔티티 연관관계 설정
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);
}
}
📌 다대다 양방향 연관관계 테스트
@SpringBootTest
class ProducerRepositoryTest {
//..생략..
@Test
@Transactional
void relationshipTest2() {
Product product1 = saveProduct("동글펜", 500, 1000);
Product product2 = saveProduct("네모 공책", 100, 2000);
Product product3 = saveProduct("지우개", 152, 1234);
Producer producer1 = saveProducer("flature");
Producer producer2 = saveProducer("wikibooks");
producer1.addProduct(product1);
producer1.addProduct(product2);
producer2.addProduct(product2);
producer2.addProduct(product3);
product1.addProducer(producer1);
product2.addProducer(producer1);
product2.addProducer(producer2);
product3.addProducer(producer2);
producerRepository.saveAll(Lists.newArrayList(producer1, producer2));
productRepository.saveAll(Lists.newArrayList(product1, product2, product3));
System.out.println("products : " + producerRepository.findById(1L).get().getProducts());
System.out.println("producers : " + productRepository.findById(2L).get().getProducers());
}
//..생략..
}
: 특정 엔티티의 영속성 상태 변경 시 그 엔티티와 연관된 엔티티의 영속성에도 영향을 미쳐 영속성 상태를 변경하는 것
📌 ex) @OneToMany 어노테이션의 인터페이스
📌 영속성 전이 타입의 종류
종류 | 설명 |
ALL | 모든 영속 상태 변경에 대해 영속성 전이를 적용 |
PERSIST | 엔티티가 영속화할 때 연관된 엔티티도 함께 영속화 |
MERGE | 엔티티를 영속성 컨텍스트에 병합할 때 연관된 엔티티도 병합 |
REMOVE | 엔티티를 제거할 때 연관된 엔티티도 제거 |
REFRESH | 엔티티를 새로고침할 때 연관된 엔티티도 새로고침 |
DETACH | 엔티티를 영속성 컨텍스트에서 제외하면 연관된 엔티티도 제외 |
📌 공급업체 엔티티에 영속성 전이 설정
: 엔티티를 DB에 추가하는 경우로 영속성 전이 타입으로 PERSIST를 지정
public class Provider extends BaseEntity {
//..생략..
@OneToMany(mappedBy = "provider", cascade = CascadeType.PERSIST)
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
}
📌 영속성 전이 테스트
@Test
void cascadeTest() {
Provider provider = savedProvider("새로운 공급업체");
Product product1 = savedProduct("상품1", 1000, 1000);
Product product2 = savedProduct("상품2", 500, 1500);
Product product3 = savedProduct("상품3", 750, 500);
// 연관관계 설정
product1.setProvider(provider);
product2.setProvider(provider);
product3.setProvider(provider);
provider.getProductList().addAll(Lists.newArrayList(product1, product2, product3));
providerRepository.save(provider);
}
private Provider savedProvider(String name) {
Provider provider = new Provider();
provider.setName(name);
return provider;
}
private Product savedProduct(String name, Integer price, Integer stock) {
Product product = new Product();
product.setName(name);
product.setPrice(price);
product.setStock(stock);
return product;
}
: JPA에서 고아란 부모 엔티티와 연관관계가 끊어진 엔티티를 의미. JPA에는 고아 객체를 자동으로 제거하는 기능 제공. 자식 엔티티가 다른 엔티티와 연관관계를 가지고 있다면 제거 기능은 비추천.
📌 공급업체 엔티티에 고아 객체를 제거하는 기능을 추가
public class Provider extends BaseEntity {
//..생략..
@OneToMany(mappedBy = "provider", cascade = CascadeType.PERSIST, orphanRemoval = true)
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
}
📌 고아 객체의 제거 기능 테스트
@Test
@Transactional
void orphanRemovalTest() {
Provider provider = savedProvider("새로운 공급업체");
Product product1 = savedProduct("상품1", 1000, 1000);
Product product2 = savedProduct("상품2", 500, 1500);
Product product3 = savedProduct("상품3", 750, 500);
// 연관관계 설정
product1.setProvider(provider);
product2.setProvider(provider);
product3.setProvider(provider);
provider.getProductList().addAll(Lists.newArrayList(product1, product2, product3));
providerRepository.saveAndFlush(provider);
System.out.println("## Before Removal ##");
System.out.println("## provider list ##");
providerRepository.findAll().forEach(System.out::println);
System.out.println("## product list ##");
productRepository.findAll().forEach(System.out::println);
// 연관관계 제거
Provider foundProvider = providerRepository.findById(1L).get();
foundProvider.getProductList().remove(0);
System.out.println("## After Removal ##");
System.out.println("## provider list ##");
providerRepository.findAll().forEach(System.out::println);
System.out.println("## product list ##");
productRepository.findAll().forEach(System.out::println);
}
1. 일대다 연관관계이며 주인 객체가 provider이고 fetch 전략이 즉시 로딩이 되도록 빈칸을 채우시오.
public class Provider extends BaseEntity {
//..생략..
/*빈칸*/
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
}
2. 다음 코드의 빈칸에 엔티티가 영속화할 때 연관된 엔티티도 함께 영속화하고 고아 객체를 제거하도록 작성하시오.
public class Provider extends BaseEntity {
//..생략..
@OneToMany(mappedBy = "provider", /*빈칸*/)
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
}
1번 답
public class Provider extends BaseEntity {
//..생략..
@OneToMany(mappedBy = "provider", fetch = FetchType.EAGER) //빈칸 답
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
}
2번 답
public class Provider extends BaseEntity {
//..생략..
@OneToMany(mappedBy = "provider", cascade = CascadeType.PERSIST, orphanRemoval = true)
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
}
[스프링 1팀] 12장. 서버 간 통신 (0) | 2025.01.24 |
---|---|
[스프링 1팀] 10-11장. 유효성 검사와 예외처리 및 액츄에이터 (0) | 2025.01.17 |
[스프링 1팀] 8장. Spring Data JPA 활용 (0) | 2025.01.03 |
[스프링 1팀] 7장. 테스트 코드 작성하기 (0) | 2024.12.27 |
[스프링 1팀] 6장 데이터베이스 연동 (0) | 2024.11.29 |