지금까지 사용해온 Product 엔티티를 대상으로 일대일로 매핑될 상품 정보 테이블을 생성해보자!
위와 같이 하나의 상품에 하나의 상품정보만 매핑되는 구조
-> 일대일 관계
상품 정보 엔티티(ProductDetail.java)
@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
어노테이션은 다른 엔티티 객체를 필드로 정의했을 때 일대일 연관관계로 매핑하고 위해 사용
@JoinColumn
을 통해 매핑할 외래키 설정
-> 기본값이 설정돼 있어 자동으로 이름을 매핑하지만 의도하지 않는 이름이 들어가기에 name 속성을 통해 원하는 칼럼명을 지정하자
-> 이 어노테이션 선언안하면 엔티티 매핑 중간에 테이블이 생겨 관리 포인트가 늘어남
name
: 매핑할 외래키의 이름 설정
referencedColumnName
: 외래키가 참조할 상대 테이블의 칼럼명 지정
foreignKey
: 외래키를 생성하면서 지정할 제약조건 설정(nullable, unique 등)
이를 완료시 단방향 일대일 관계
완성 됨
애플리케이션 실행 시 Hibernate에서 자동으로 테이블을 생성하여 위와 같은 Data Table 확인 가능
생성된 상품 정보 entity 객체를 사용하기 위해 리포지토리 생성
public interface ProductDetailRepository extends JpaRepository<ProductDetail, Long> {
}
연관관계를 활용한 데이터 생성 및 조회 기능을 테스트 코드로 작성
@SpringBootTest
public class ProductDetailRepositoryTest {
@Autowired
ProductDetailRepository productDetailRepository;
@Autowired
ProductRepository productRepository;
@Test
public void saveAndReadTest(){
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());
}
상품과 상품정보에 매핑된 리포지토리에 대해 의존성 주입
ProductDetail 객체에서 Product 객체를 일대일 단방향 연관관계를 설정하여 ProductDetailRepository에서 ProductDetail 객체를 조회한 후 연관 매핑된 Product 객체 조회 가능
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_
left outer join
product product1_
on productdet0_.product_number=product1_.number
where
productdet0_.id=?
select -> ProductDetail 객체와 Product 객체가 함께 조회
엔티티를 조회할 때 연관된 엔티티도 함께 조회하는 것 -> 즉시 로딩
@OneToOne 어노테이션
에 의해 left outer join
이 수행됨
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
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;
}
fetch 전략으로 EAGER -> 즉시 로딩 전략이 채택
optional() 메서드 -> 디폴트 값이 true(매핑 되는 값이 nullable 이라는 의미)
반드시 값이 있어야 한다면 @OneToOne(optional = false)
-> Product가 null인 값을 허용하지 않겠다!
그리고 left outer join이 inner join으로 바뀌어 실행
-> 객체에 대한 설정에 따라 JPA는 최적의 쿼리를 생성해서 실행
객체에서의 양방향 개념은 양쪽에서 단방향으로 서로를 매핑하는 것
을 의미
일대일 양방향 매핑을 위한 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
ProductDetail productDetail;
}
Product detail에도 칼럼이 생성되는 것을 볼 수 있음
-> 위 경우 left outer join이 두번 실행 됨
여러 테이블끼리 연관관계가 설정돼 있어 여러 left outer join이 설정되는 것은 괜찮으나 위와같이 양쪽에서 외래키를 가지고 left outer join이 두번이나 수행되는 경우는 효율성이 떨어짐
-> 실제 DB에서도 연관관계를 맺으면 한쪽 테이블이 외래키를 가지는 구조로 이루어짐(Owner
개념 참조)
JPA에서도 실제 DB 연관관계를 반영해 한쪽의 테이블에서만 외래키를 바꿀 수 있도록 지정하는 것이 좋음
-> 엔티티는 양방향으로 매핑, 한쪽만 외래키를 가지게 하기 위해 mappedBy
사용(어떤 객체가 Owner 인지 표시)
Product 엔티티 클래스에 mappedBy 속성 추가
@OneToOne(mappedBy = "product")
ProductDetail productDetail;
mappedBy는 상대 엔티티에 있는 연관관계 필드의 이름
위 경우 ProductDetail 엔티티가 Product entity의 Owner가 됨
이대로 애플리케이션 실행 시 toString 실행 시점에 StackOverflowError 발생
양방향으로 연관관계가 설정되면 tostring 사용 시 순환 참조 발생
-> 필요한 경우가 아니라면 대체로 단방향으로 연관관계를 설정
하거나 양방향 설정이 필요한 경우엔 순환 참조를 제거하자
@OneToOne(mappedBy = "product")
@ToString.Exclude
ProductDetail productDetail;