[Spring] 일대일 매핑

WOOK JONG KIM·2022년 11월 2일
0
post-thumbnail

지금까지 사용해온 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이 수행됨

OneToOne어노테이션 인터페이스

@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;
profile
Journey for Backend Developer

0개의 댓글