연관관계를 맺는 두 엔티티 간에 생성할 수 있는 연관관게의 종류는 총 4가지다.
데이터베이스: 외래키를 통해 서로 조인, 참조
JPA: 엔티티 간 참조 방향 설정 가능
데이터베이스와 관계를 일치시키기 위해 양방향으로 설정해도 무관하지만, 비즈니스 로직의 관점에서는 단방향 관계만 설정해도 해결되는 경우가 많다.
<<단방향과 양방향 형식 참고>>
연관관계가 설정되면 다른 테이블의 기본값을 외래키로 갖게 됩니다. 이때 쓰이는 것이 주인(Owner)라는 개념이다.
일반적으로 외래키를 가진 테이블이 그 관계의 주인이 된다. 주인은 외래키를 사용 가능하나 상대 엔티티는 읽는 작업만 수행할 수 있다.
실습을 위해 새로운 프로젝트를 생성한다.
위와 같이 하나의 상품에 하나의 상품정보만 매핑되는 구조를 일대일 관계라고 한다.
@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;
@0neTo0ne
@3oinColumn(name = "product_number") private Product product;
}
6.7절에서 엔티티를 작성했던 방법 그대로 상품정보 엔티티를 작성한다. 그리고 상품 번호에 매핑하 기 위해 16~18번 줄과 같이 작성합니다. @0neT00ne 어노테이션은 다른 엔티티 객체를 필드로 정의했 을 때 일대일 연관관계로 매핑하기 위해 사용된다.
뒤이어 @Joincolumn 어노테이션을 사용해 매핑할 외래키를 설정한다. @JoinColumn 어노테이션은 기본값이 설정돼 있어 자동으로 이름을 매핑하지만 의 도한 이름이 들어가지 않기 때문에 name 속성을 사용해 원하는 칼럼명을 지정하는 것이 좋다.
만약 @JoinColumn을 선언하지 않으면 엔티티를 매핑하는 중간 테이블이 생기면서 관리 포인트가 늘어나 좋지 않다. 간단하게 @JoinColumn 어노테이션에서 사용할 수 있는 속성을 설명하면 다음과 같다.
- name: 매핑할 외래키는 이름을 설정
- referencedColumName: 외래키가 참조할 상대 테이블의 칼럼명을 지정
- foreignKey: 외래키를 생성하면서 지정할 제약조건을 설정(unique, nullable, insertable, updatable 등)
엔티티 클래스를 생성하면 단방향 관계의 일대일 관계 매핑이 완성된다. hibernate.ddl-auto의 값을 create로 설정한 후 애플리케이션을 실행하면 하이버네이트에서 자동으로 테이블을 생성하며 그림 9.4 와 같이 데이터베이스의 테이블을 확인할 수 있다.
생성된 상품정보 엔티티 객체들을 사용하기 위해 리포지토리 인터페이스를 생성한다.
public interface ProductDetailRepository extends 3paRepository<ProductDetail, Long> {
}
그럼 연관관계를 활용한 데이터 생성 및 조회 기능을 테스트 코드로 간략하게 작성해보자.
package com.springboot.relationship.data.repository;
import com.springboot.relationship.data.e n tity .Product;
import com.springboot.relationship.data.entity.ProductDetail; import org.ju n it.ju p ite r.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.te s t.context.SpringBootTest;
@SpringBootTest
class ProductDetailRepositoryTest {
@Autowired
ProductDetailRepository ProductDetailRepository;
@Autowired ProductRepositoryProductRepository;
@Test
public voidsaveAndReadTestl() {
Product product = new ProductO;
product.setName("스프링 부트 JPA");
product.setPrice(5000);
product.setStock(500);
productRepository.save(product);
ProductDetail productDetail = new ProductDetailO; productDetail.setProduct(product);
productDetail.setDescription("스프링 부트와 JPA를 함께 볼 수 있는 책");
productDetailRepository.save(productDetail);
// 생성한 데이터 조회
System.out.println("savedProduct : " + productDetailRepository.findById(productDetail.getld()).get().getProduct());
System.out.p rin tin ("savedProductDetail : " + productDetailRepository.findById( productDetail.getId()).get());
}
}
위와 같은 테스트 코드를 실행하기 위해서는 12- 16번 줄과 같이 상품과 상품정보에 매핑된 리포지토리 에 대해 의존성 주입을 받아야 한다. 그리고 이 테스트에서 조회할 엔티티 객체를 20- 31 번 줄과 같이 저장한다. 여기서 주요 코드는 34-35번줄이다.
ProductDetail 객체에서 Product 객체를 일대일 단방향연관관계를 설정했기 때문에 ProductDetailRepository에서ProductDetail 객체를 조회한 후 연관매핑된 Product 객체를 조회할 수 있다. 34-35번 줄과 37-38번 줄에서 조회하는 쿼리는 다음과 같이 표현된다.
Hibernate:
select
productdet0_.id as id1_1_0_,
productdet0_,created_at as created_2_1_0_,
productdet0_.description as descript3_1_0_,
productdet0_,product_number as product_5_1_0_,
productdet0_.updated_at as updated_4_1_0_,
product1_.number as number1_0_1_z
product1_.created_at as created_2_0_1_,
product1_.name as name3_0_1_,
product1_.price as price4_0_1_,
product1_.stock as stock5_0_1_,
product1_.updated_at as updated_6_0_1_
from
product_detail productdet0_
left outer join
product product1_
on productdet0_.product_number=product1_.number
where
productdet0_.id=?
select 구문을 보면 ProductDetail 객체와 Product 객체가 함께 조회되는 것을 볼 수 있다. 이처럼 엔티티를 조회할 때 연관된 엔티티도 함께 조회하는 것을 ‘즉시 로딩’이라고 한다.
그리고 16‒ 18번 줄에서 left outer join이 수행되는것을 볼수있다. 여기서 left outer join이 수행되는이유는 @OneToOne 어노테이션 때문이다. 예제 9.4 에서 @0neTo0ne 어노테이션 인터페이스를 확인하자.
9.4 @OneToOne 어노테이션 인터페이스
01 public ©interface OneToOne {
02
03 Class targetEntityO default void.class; 04
05 CascadeType[] cascadeO default { } ;
06
07 FetchType fetch() default EAGER;
08
09 boolean optionaK) default true;
10
11 String mappedByO default "";
12
13 boolean orphanRemovaK) default false;
14 }
이후에 더 자세히 살펴볼 예정이므로 여기서는 fetch() 요소와 optionaK) 요소만 보자. @0neTo0ne 어노테이션은 기본 fetch 전략으로 EAGER, 즉 즉시 로딩 전략이 채택된 것을 볼 수 있다. 그리고 optionaK) 메서드는 기본값으로 true가 설정돼 있다. 기본값이 true인 상태는 매핑되는 값이 nullable이라는 것을 의미한다. 반드시 값이 있어야 한다면 ProductDetail 엔티티에서 속성값에 예제 9.5 와 같이 설정할 수 있다.
01 @Entity
02 @Table(name = "product_detail")
03 @Getter
04 @Setter
05 @NoArgsConstructor
06 @ToString(callSuper = true)
07 @EqualsAndHashCode(callSuper = true)
08 public class ProductDetail extends BaseEntity {
09
10 @Id
11 @GeneratedValue(strategy = GenerationType.IDENTITY)
12 private Long id;
13
14 private String description; 15
16 @0neToOne(optional = false)
17 @JoinColumn(name = "product_number")
18 private Product product;
19
20 }
위와 같이 ©OneToOne 어노테이션에 ,optional = false’ 속성을 설정하면 product가 null인 값을 허용하지 않게 된다. 위와 같이 설정하고 애플리케이션을 실행하면 다음과 같이 테이블을 생성하는 쿼리에서 not null이설정되는것을 확인할 수 있다.
Hibernate:
create table product_detail (
id bigint not null auto_increment,
created_at datetime(6)z
updated_at datetime(6),
description varchar(255),
product_number bigint notn니11,
primary key (id)
) engine=InnoDB
Hibernate:
select
productdet0_.id as id1_1_0_, productdet0_.created_at as created_2_1_0_, productdet0_.updated_at as updated_3_1_0_, productdet0_.description as descript4_1_0_, productdet0_.product_number as product_5_1_0_, product1_.number as number1_0_1_,
product1_.created_at as created_2_0_1_,
product1_.updated_at as updated_3_0_1_, product1_.name as name4_0_1_,
product1_.price as price5_0_1_,
product1_.stock as stock6_0_1_
from
product_detail productdet0_
inner join
product product1_
on productdet0_.product_number=product1_.number
where
productdet0_.id=?
즉, @0neTo0ne 어노테이션에 'optional = false, 속성을 지정한 경우에는 16‒18번줄과 같이left outer join이 inner join으로 바뀌어 실행된다. 이처럼 객체에 대한 설정에 따라 JPA는 최적의 쿼리를 생성해서 실행한다.
이후 내용을 진행하기 위해 @0neTo0ne에 적용했던 ,optional = false, 속성은 제거한다.
이번에는 앞에서 생성한 일대일 단방향 설정을 양방향 설정으로 변경해보자. 사실 객체에서의 양방향 개념은 양쪽에서 단방향으로 서로를 매핑하는 것을 의미한다. 일대일 양방향 매핑을 위해서는 예제 9.6 과 같이 Product 엔티티를 추가한다.
@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString(callSuper = true) @EqualsAndHa와Code(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;
}
Hibernate:
select
productdet0_,id as id1_1_0_,
productdet0_.created_at as created_2_1_0_,
productdet0_.updated_at as updated_3_1_0_,
productdet0_.description as descript4_1_€)__,
productdet0_.product_number as
product_5_1_0_,
product1_.number as number1_0_1_,
product1_.created_at as created_2_0_1_,
product1_.updated_at as updated_3_0_1_,
product1_.name as name4_0_1_,
product1_.price as price5_0_1_z product1_.product_detail_id as product_7_0_1_, product1_.stock as stock6_0_1_, productdet2_.id as id1_1_2_z productdet2_.created_at as created_2_1_2_, productdet2_.updated_at as updated_3_1_2_, productdet2_.description as descript4_1_2_,
productdet2_.prochjct_number as product_5_1_2_
from
product_detail productdet0_
left outer join
product product1_
on productdet0_.product_number=product1_.number
left outer join
product_detail productdet2_
on product1_.product_detail_id=productdet2_.id
where
productdet0_.id=?
여러 테이블끼리 연관관계가 설정돼 있어 여러 left outer join이 설정되는 것은 괜찮으나 위와 같이 양 쪽에서 외래키를 가지고 left outer join이 두 번이나 수행되는 경우는 효율성이 떨어진다.
실제 데이 터베이스에서도 테이블 간 연관관계를 맺으면 한쪽 테이블이 외래키를 가지는 구조로 이뤄진다. 바로 앞에서 언급한 ‘주인’개념이다.
JPA에서도 실제 데이터베이스의 연관관계를 반영해서 한쪽의 테이블에서만 외래키를 바꿀 수 있도록 정하는 것이 좋다. 이 경우 엔티티는 양방향으로 매핑하되 한쪽에게만 외래키를 줘야 하는데, 이때 사용되는 속성 값이 mappedBy이다.
mappedBy는 어떤 객체가 주인인지 표시하는 속성이라고 볼 수 있다. 예제 9.7과같이 Product 객체에 mappedBy 속성을 추가해 보자.
01 @Entity
02 @Getter
03 @Setter
04 @NoArgsConstructor
05 @ToString(callSuper = true)
06 @EqualsAndHashCode(callSuper
07 @Table(name = "product")
= true)
08 public class Product extends BaseEntity{
09
10 @Id
11 @GeneratedValue(strategy =GenerationType.IDENTITY)
12 private Long number;
13
14 @Column(nullable = false)
15 private String name;
16
17 @Column(nullable = false)
18 private Integer price;
19
20 @Column(nullable = false)
21 private Integer stock;
22
23 @0neToOne(mappedBy = “product")
24 private ProductDetail productDetail;
25
26 }
23-24번 줄을 보면 ©OneToOne 어노테이션에 mappedBy 속성값을 사용했다. mappedBy에 들어가 는 값은 연관관계를 갖고 있는 상대 엔티티에 있는 연관관계 필드의 이름이 된다. 이 설정을 마치면 ProductDetail 엔티티가 Product 엔티티의 주인이 되는 것이다. 애플리케이션을 실행하고 HeidiSQL 에서 데이터베이스의 테이블을 보면 그림 9.6과 같이 외래키가 사라진 것을 볼 수 있다.
그리고 다시 테스트 코드를 실행하면 toString을 실행하는 시점에서 StackOverflowError가 발생하는 것을 볼 수 있다. 양방향으로 연관관계가 설정되면 ToString을 사용할 때 순환참조가 발생하기 때문이다. 그렇기 때문에 필요한 경우가 아니라면 대체로 단방향으로 연관관계를 설정하거나 양방향 설정이 필요할 경우에는 순환참조 제거를 위해 예제 9.8과 같이 exclude를 사용해 ToString에서 제외 설정을 하 는 것이 좋다.
01 @0neToOne(mappedBy = "product")
02 @ToString.Exclude
03 private ProductDetail productDetail;
위와 같이 Product 엔티티 클래스의 코드를 수정하면 기존 테스트 코드가 정상적으로 동작하는 것을 볼 수 있다.
상품 테이블과 공급업체 테이블은 그림 9.7과 같이 상품 테이블의 입장에서 볼 경우에는 다대일, 공급업 체 테이블의 입장에서 볼 경우에는 일대다 관계로 볼 수 있다. 이런 관계는 어떻게 구현해야 할지 직접 매핑하면서 알아보자.
©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를 통해 생성일 자와 변경일자를 상속받는다.
상품 엔티티에서는 공급업체의 번호를 받기 위해 다음과 같이 엔티티 필드의 구성을 추가해야 한다. 예제 9.10과 같이 상품 엔티티에 필드를 추가하자.
01 @Entity
02 @Getter
03 @Setter '
04 @NoArgsConstructor
05 @ToString(callSuper = true)
06 @EqualsAndHashCode(callS니per = true)
07 @Table(name = "product")
08 public class Product extends BaseEntity {
09
10 @Id
11 @GeneratedValue(strategy= GenerationType.IDENTITY)
12 private Long number;
13
14 @Column(nullable = false)
15 private String name;
16
17 @Column(nullable = false)
18 private Integer price;
19
20 @Column(nullable = false)
21 private Integer stock;
22
23 @0neToOne(mappedBy = "product")
24 @ToString.Exclude
25 private ProductDetail productDetail;
26
27 @ManyToOne
28 @3oinColumn(name = "provider_id")
29 @ToString.Exclude
30 private Provider provider;
31
32 }
예제 9.10의 27- 29번 줄은 공급업체 엔티티에 대한 다대일 연관관계를 설정한다. 일반적으로 외래 키를 갖는 쪽이 주인의 역할을 수행하기 때문에 이 경우 상품 엔티티가 공급업체 엔티티의 주인이다.
위와 같이 설정한 후 애플리케이션을 가동하면 그림 9.8과 그림 9.9와 같이 Product 테이블과 Provider 테이블이 생성된다.
01 public interface ProviderRepository extends 3paRepository<Provider, Long> {
02
03 }
01 @SpringBootTest
02 class ProviderRepositoryTest {
03
04 @Autowired
05 ProductRepository productRepository;
06
07 @Autowired
08 ProviderRepositoryProviderRepository;
09
10 @Test
11 void relationshipTestK) {
12 // 테스트 데이터 생성
13 Provider provider = new Provider();
14 provider.setName(“o o물산");
15
16 ProviderRepository.save(provider) ; 17
18 Product product = new ProductO;
19 product.setName("가위,,);
20 product.setPrice(5000);
21 product.setStock(500);
22 product.setProvider(provider);
23
24 productRepository.save(product); 25
26 // 테스트
27 System.out.println(
28 "product : “ + productRepository.findById(1L)
29 .orElseThrow(RuntimeException: :new) ) ;
30
31 System.out.println("provider : " + productRepository.findById(1L)
32 .orElseThrow(RuntimeException::new).getProvider());
33 }
34 }
이제 각 엔티티의 연관관계를 테스트하기 위해 테스트 데이터를 생성해야 한다.
애플리케이션을 실행했을 때 하이버네이트로 생성된 쿼리를 보면 다음과 같다.
Hibernate:
insert
into
product
(created_at, updated_at, name, price, provider_id, stock)
values
(?, ?, ?, ?, ?, ?)
쿼리로 데이터를 저장할 때는 provider_id 값만 들어가는 것을 볼 수 있다. 이렇게 product 테이블에 는 @Joincolumn에 설정한 이름을 기반으로 자동으로 값을 선정해서 추가하게 된다.
앞서 상품 엔티티와 공급업체 엔티티 사이에 다대일 단방향 연관관계를 설정했다. 반대로 일대다 연관관계를 설정해보자.
01 @Entity
02 @Getter
03 @Setter
04 @NoArgsConstructor
05 @ToString(callSuper = true)
06 @EqualsAndHzhCode(callSuper = true)
07 @Table(name = "provider")
08 public class Provider extends BaseEntity {
09
10 @Id
11 @GeneratedValue(strategy = GenerationType.IDENTITY)
12 private Long id;
13
14 private String name; 15
16 @OneToMany(mappedBy = "provider", fetch = FetchType.EAGER)
17 @ToString.Exclude
18 privateList<Product> productList=newArrayList<>();
19
20 }
일대다 연관관계의 경우 여러 상품 엔티티가 포함될 수 있어 18번 줄과 같이 컬렉션(Collection, List, Map) 형식으로 필드를 생성한다.
지연로딩과 즉시로딩
JPA에서 지연로딩과 즉시로딩은 중요한 개념이다. 엔티티라는 객체의 개념으로 데이터베이스를 구현했기 때문에 연관관계를 가진 각 엔티티 클래스에는 연관관계가 있는 객체들이 필드에 존재하게 된다.
연관관계와 상관없이 즉각 해당 엔티티의 값만 조회하고 싶거나 연관관계를 가진 테이블의 값도 조회하고 싶은 경우 등 여러 조 건들을 만족하기 위해 등장한 개념이 지연로딩과 즉시로딩이다.
01 @Autowired
02 ProductRepository productRepository;
03
04 @Autowired
05 ProviderRepository providerRepository;
06
07 @Test
08 void relationshipTestO {
09
10 // 테스트 데이터 생성
11 Provider provider = new Provider();
12 provider.setName(00상사);
13
14 providerRepository.save(provider); 15
16 Product productl = new ProductO;
17 productl .setName("펜', );
18 productl.setPrice(2000);
19 productl.setStock(100);
20 productl.setProvider(provider);
21
22 Product product2 = new ProductO;
23 product2.setName("가 방 ");
24 product?.setPrice(20000);
25 product?.setStock(200);
26 product2.setProvider(provider);
27
28 Product products = new ProductO;
29 product3.setName("노트");
30 products.setPrice(3000);
31 products.setStockd000);
32 products.setProvider(provider);
33
34 productRepository.save(productl);
35 productRepository.save(product2);
36 productRepository.save(product3);
37
38 List<Product> products = providerRepository.findById(provider.getId()).get()
39 .getProductList();
40
41 for(Product product : products)!
42 System.out.println(product);
43 }
44
45 }
Provider 엔티티 클래스는 Product 엔티티와의 연관관계에서 주인이 아니기 때문에 외래키를 관리할 수 없다. 그렇기 때문에 테스트 데이터를 생성하는 11‒ 36번 줄과 같이 Provider를 등록한 후 각 Product에 객체를 설정하는 작업을 통해 데이터베이스에 저장한다. 만약 앞의 예제에서 테스트 데이터를 생성하는 빙식이 아니라 Provider 엔티티에 정의한 productList 필드에 예제 9.15와 같이 Product 엔티티를 추가하는 방식으로 데이터베이스에 레코드를 저장하게 되면 Provider 엔티티 클래스는 연관관계의 주인이 아니기 때문에 해당 데이터는 데이터베이스에 반영되지 않는다.
주인이 아닌 엔티티에서 연관관계를 설정한 예
01 provider.getProductList().add(productl);// 무시 02 provider.getProductList().add(product?);// 무시 03 provider.getProductList().add(products);// 무시
이번에는 일대다 단방향 매핑 방 법을 알아보자. 참고로 일대다 양방향 매핑은 다루지 않을 예정이다. 그 이유는 ©OneToMany를 사용하는 입장에서는 어느 엔티티 클래스도 연관관계의 주인이 될 수 없기 때문이다.
상품 분류 엔티티 클래스를 생성하고 애플리케이션을 실행하면 그림 9.11 과 같이 상품 분류 (category) 테이블이 생성되고 그림9.12와 같이 상품테이블에 외래키가 추가되는것을 확인할 수 있다.
상품 분류 엔티티에서 @OneToMany와 @JoinColumn을 사용하면 상품 엔티티에서 별도의 설정을 하지 않아 도 일대다 단방향 연관관계가 매핑된다. 앞에서 언급한 것처럼 @JoinColumn 어노테이션은 필수 사항은 아니다. 이 어노테이션을 사용하지 않으면 중간 테이블로 Join 테이블이 생성되는 전략이 채택된다.
현재는 단방향 매핑이 완료된 상태다. 지금 같은 일대다 단방향 관계의 단점은 매핑의 주체가 아닌 반대 테이블에 외래키가 추가된다는 점이다. 이 방식은 다대일 구조와 다르게 외래키를 설정하기 위해 다른 테이블에 대한 update 쿼리를 발생시킨다.
카테고리레포지토리를 생성하고 테스트 코드를 작성해보자.
01 @SpringBootTest
02 class CategoryRepositoryTest {
03
04 @Autowired
05 ProductRepository productRepository;
06
07 @Autowired
08 CategoryRepository CategoryRepository;
09
10 @Test
11 void relationshipTest(){
12 // 테스트 데이터 생성
13 Prod니ct prod니ct = new Prod니ct();
14 product.setName("펜);
15 product.setPrice(2000);
16 product.setStock(100);
17
18 productRepository.save(product) ;
19
20 Category category = new CategoryO;
21 category.setCode("S1");
22 category.setName("도서");
23 category.getProducts( ).add(product) ;
24
25 categoryRepository.save(category);
26
27 // 테스트 코드
28 List<Product> products = categoryRepository.findById(1L).get().getProducts() ;
29
30 for(Product foundProduct : products){
31 System.out.printin(product);
32 }
33 }
34 }
테스트 데이터 생성을 위해 ProductRepository의 의존성도 함께 주입받자.
Hibernate:
insert
into product
(created_at, updated_atz name, price, provider_idz stock) values
(?,?, ?,?,?,?)
Hibernate:
insert into
category
(code, name) values
(?, ?)
Hibernate:
update
product set
category_id=? where
number=?
일대다 연관관계에서는 위와 같이 연관관계 설정을 위한 update 쿼리가 발생한다. 이 같은 문제를 해결 하기 위해서는 일대다 양방향 연관관계를 사용하기보다는 다대일 연관관계를 사용하는 것이 좋다.
01 Hibernate:
02 select
03 category0_,id as id1_0_0_,
04 category0_.code as code2_0_0_,
05 category0_.name as name3_0_0_,
06 products1_.category_id as category8_1_1_,
07 products1_.number as number1_1_1_,
08 products1_.number as number1_1_2_,
09 products1_.created_at as created_2_1_2_,
10 products1_.updated_at as updated_3_1_2_,
11 productsV.name as name4_1_2_,
12 products1_.price as price5_1_2_,
13 products1_.provider_id as provider7_1_2_z
14 products1_.stock as stock6_1_2_,
15 provider2_.id as id1_3_3_,
16 provider2_.created_at as created_2_3_3_,
17 provider2_.updated_at as updated_3_3_3_,
18 provider2_.name as name4_3_3_,
19 productdet3_.id as id1_2_4_,
20 productdet3_.created_at as created_2_2_4_,
21 productdet3_.updated_at as updated_3_2_4_z
22 productdet3_.description as descript4_2_4_,
23 productdet3_.product_number as product_5_2_4_
24 from
25 category category0_
26 left outer join
27 product products1_
28 on category0_.id=products1_.category_id
29 left outer join
30 provider provider2_
31 on products1_.provider_id=provider2_.id
32 left outer join
33 product_detail productdet3_
34 on products1_.number=productdet3_.product_number
35 where
36 categoryO_.id=?
일대다 연관관계에서는 이처럼 category와 product의 조인이 발생해서 상품 데이터를 정상적으로 가져온다.
다대다(M :N)연관관계는 실무에서 거의 사용되지 않는 구성이다.
01 @Entity
02 @Getter
03 @Setter
04 @NoArgsConstructor
05 @ToString(callSuper = true)
06 @EqualsAndHa와Code(callSuper = true)
07 @Table(name = "producer")
08 public class Producer extends BaseEntity{
09
10 @Id
11 @GeneratedValue(strategy = GenerationType.IDENTITY)
12 private Long id;
13
14 private String code;
15
16 private String name;
17
18 @ManyToMany
19 @ToString.Exclude
20 private List<Product> products = new ArrayList<>();
21
22 public void addProduct(Product product){
23 products.add(product) ;
24 }
25
26 }
생산업체 테이블에는 별도의 외래키가 추가되지 않은 것을 볼 수 있다. 그리고 데이터베이스에 추가로 중간 테이블이 생성돼 있다.
이제 연관관계를 테스트하기 위해 생산업체 엔티티에 대한 리포지토리를 생성하고 연관관계가 정상적으로 동작하는지 확인해보자.
01 @Autowired
02 ProducerRepository producerRepository;
03
04 @Autowired
05 ProductRepository productRepository;
06
07 @Test
08 @Transactional
09 void relation와ipTest() {
10
11 Product productl = saveProduct("동 글 펜 ", 500, 1000);
12 Product product2 = saveProduct("네 모 공 책 ", 100, 2000);
13 Product product3 = saveProduct("지 우 개 ", 152, 1234);
14
15 Producer producer1 = saveProducer("flature");
16 Producer producer2 = saveProducer("wikibooks");
17
18 produceri.addProduct(productl);
19 produceri.addProduct(product2);
20
21 producer2.addProduct(product2);
22 producer2.addProduct(product3);
23
24 producerRepository.saveAlKLists.newArrayList(produceri, producer』));
25
26 System.out.println(producerRepository.findById(1L).get().getProducts()); 27
28 }
29
30 private Product saveProduct(String name, Integer price, Integer stock) {
31 Product product = new Product();
32 product.setName(name);
33 product.setPrice(price);
34 product.setStock(stock);
35
36 return productRepository.save(product);
37 }
38
39 private Producer saveProducer(String name) {
40 Producer producer = new ProducerO;
41 producer.setName(name);
42
43 return producerRepository.save(producer);
44 }
위 예제에서는 가독성을 위해 리포지토리를 통해 테스트 데이터를 생성하는 부분을 별도 메서드로 구현 했다. 이 경우 리포지토리를 사용하게 되면 매번 트랜잭션이 끊어져 생산업체 엔티티에서 상품 리스트를 가져오는 작업이 불가능하다. 이 같은 문제를 해소하기 위해 테스트 메서드에 @Transactional 어노테이션을 지정해 트랜잭션이 유지되도록 구성해서 테스트를 진행한다.
연관관계를 설정했기 때문에 정상적으로 생산업체 엔티티에서 상품 리스트를 가져오는 것을 볼 수 있다.
테스트를 통해 테스트 데이터를 생성하면 product 테이블과 producer 테이블에 레코드가 추가되지만 보여지는 내용만으로는 연관관계 설정 여부를 확인하기 어렵다. 그 이유는 다대다 연관관계 설정을 통해 생성된 중간 테이블에 연관관계 매핑이 돼 있기 때문이다. 중간 테이블에 생성된 레코드를 확 인하면 그림 9.16과 같다.
다대다 단방향 매핑의 개념을 이해했다면 양방향 매핑을 하는 방법은 아주 간단하다.
생산업체에 대한 다대다 연관관계를 설정하고, 필요에 따라 주인을 설정할 수도 있다. 중간 테이블이 연관관계를 설정하고 있기 때문에 애플리케이션을 실행하면 테이블 구조는 변경되지 않는다.
다대다 연관관계를 설정하면 중간 테이블을 통해 연관된 엔티티의 값을 가져올 수 있다. 하지만 관리하기 힘든 포인트가 발생한다는 단점이 있으므로 중간 테이블을 생성하는 대신 일대다 다대일로 연관관계를 맺을 수 있는 중간 엔티티로 승격시켜 JPA에서 관리할 수 있게 생성하는 것이 좋다.
영속성 전이(cascade)란 특정 엔티티의 영속성 상태를 변경할 때 그 엔티티와 연관된 엔티티의 영속성 에도 영향을 미쳐 영속성 상태를 변경하는 것을 의미한다.
cascade() 요소와 함께 사용되는 영속성 전이 타입은 다음과 같다.
여기서 알 수 있듯이 영속성 전이에 사용되는 타입은 엔티티 생명주기와 연관이 있다.
이제 영속성 전이를 적용해보자. 우선 엔티티를 데이터베이스에 추가하는 경우로 영속성 전이 타입 으로 PERSIST를 지정하자.
01 @Entity
02 @Getter
03 @Setter
04 @NoArgsConstructor
05 @ToString(callSuper = true)
06 @EqualsAndHa와Code(callS니per=true)
07 @Table(name = "provider")
08 public class Provider extends BaseEntity {
09
10 @Id
11 @GeneratedValue(strategy = GenerationType.IDENTITY)
12 private Long id;
13
14 private String name;
15
16 @0neToMany(mappedBy = "provider”, cascade = CascadeType.PERSIST)
17 @ToString.Exclude
18 private List〈Product〉productList = new ArrayListOO;
19
20 }
영속성 전이 타입을 설정하기 위해서는 @OneToOne 어노테이션의 속성을 활용하자.
테스트를 수행하기 위해 3- 7번 줄과 같이 공급업체 하나와 상품 객체를 3개 생성한다. 영속성 전이를 테스트하기 위해 객체에는 영속화 작업을 수행하지 않고 연관관계만 설정해준다.
지금까지는 엔티티를 데이터베이스에 저장하기 위해 각 엔티티를 저장하는 코드를 작성했다. 하지만 영속성 전이를 사용하면 부모 엔티티가 되는 Provider 엔티티만 저장하면 코드에 작성돼 있는 Cascade.PERSIST에 맞춰 상품 엔티티도 함께 저장할 수 있다.
하지만 REMOVE와 REMOVE를 포함하는 ALL 같은 타입을 무분별하게 사용하면 연관된 엔티티가 의도치 않게 모두 삭제될 수 있기 때문에 다른 타입보다 더욱 사 이드 이펙트(side effect)를 고려해서 사용해야 한다.
JPA에서 고아(orphan)란 부모 엔티티와 연관관계가 끊어진 엔티티를 의미한다.
orophanRemoval = true
위 속성은 고아 객체를 제거하는 기능이다.
이 동작을 테스트하기 위해 고아 객체를 생성해보자. 공급업체 엔티티를 가져온 후 첫번째로 매핑돼 있는 상품 엔티티의 연관관계를 제거하면 엔티티가 제거된다.
실제로 연관관계가 제거되며 하이버네이트에서는 상태 감지를 통해 삭제하는 쿼리가 수행되며 고아 객체를 삭제시킨다.
이번 장에서는 연관관계를 설정하는 방법과 영속성 전이라는 개념을 알아봤다. JPA를 사용할 때 영속이라는 개념은 매우 중요하다.
작성자: 채드
[스프링 1팀] 12장. 서버 간 통신, [인프런] 섹션 0. 스프링 시큐리티 기본 (0) | 2023.12.29 |
---|---|
[스프링 1팀] 10-11장. 유효성 검사와 예외처리 및 액츄에이터 (0) | 2023.12.22 |
[스프링 1팀] 8장 Spring Data JPA 활용 (2) | 2023.11.24 |
[스프링1] 7장. 테스트 코드 작성하기 (0) | 2023.11.17 |
[스프링1] 6. 데이터베이스 연동 (0) | 2023.11.10 |