실무에서 테이블 하나만 사용해서 애플리케이션의 모든 기능을 구현하는 것은 불가능하다.
보통의 경우에는 테이블을 설계하고 연관관계를 설정해서 JOIN을 통해서 기능을 구현하게된다.
JPA를 사용하는 애플리케이션에서도 테이블의 연관관계를 엔티티 간의 연관관계로 표현할 수 있다.
JPA에서는 객체(Entity)를 통해서 연관관계를 설정한다.
연관관계 매핑
- One To One : 일대일
- One To Many : 일대다
- Many To One : 다대일
- Many To Many : 다대다
JPA를 사용하는 객체지향 모델링에서는 엔티티 간 참조방향을 설정할 수 있다. (데이터베이스에서는 연관관계를 설정하면 외래키를 통해 서로 조인해서 참조하는 구조로 생성된다.)
참조방향 설정
- 단방향
- 양방향
연관관계가 설정되면 한 테이블에서 다른 테이블의 기본값을 외래키로 갖고온다.
일대일로 매핑되는 일대일 관계
예를 들자면, 하나의 상품은 하나의 상품정보를 갖는다.
@OneToOne 어노테이션은 일대일 연관관계로 매핑하기 위해 사용되며, @JoinColumn 어노테이션을 사용해 매핑할 외래키를 설정한다.
다음은 단방향 관계의 일대일 관계 매핑을 한 예시이다.
상품(상품번호, 상품이름, 상품가격, 상품재고, 공급업체번호, 상품분류번호, 상품생성일자, 상품정보변경일자) 테이블과 상품정보(상품정보번호, 상품설명, 상품번호, 상품정보생성일자, 상품정보변경일자) 테이블을 일대일로 매핑시킨다고 가정하고 진행하겠다.
@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 //(optional=false) 예제 9.5
@JoinColumn(name = "product_number")
private Product product;
}
다음은 상품정보에 대한 도메인을 ProductDetail로 설정하고 진행한 코드이다.
@OneToOne 어노테이션은 다른 객체를 일대일로 매핑하기 위해 사용된다.
@JoinColumn 어노테이션은 매핑할 외래키를 설정하기 위해 사용된다.
위에서는 상품정보에 대한 도메인을 ProductDetail로 설정한 일대일 단방향 매핑을 했었다.
이러한 경우에는 한 테이블에서 다른 테이블의 기본값을 외래키로 갖고 일반적으로 외래키를 가진 테이블이 그 관계의 주인이 되며, 주인은 외래키를 사용할 수 있으나 상대 엔티티는 읽는 작업만 수행할 수 있다.
즉, ProductDetail 테이블에만 칼럼이 생성된다.
상품정보 엔티티 객체를 사용하기 위해 리포지토리 인터페이스를 생성해야한다.
public interface ProductDetailRepository extends JpaRepository<ProductDetail, Long> {
}
Product 엔티티를 추가하고, @OneToOne 어노테이션을 통해서 양방향 매핑을 해준다.
@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;
}
위와 같이 객체를 생성해주면, Product 테이블에도 칼럼이 생성된다.
양방향으로 매핑을 하게되면 양쪽에서 외래키를 가지고 left outer join이 두번 수행돼 효율성이 떨어진다.
한쪽의 테이블에서만 외래키를 바꿀 수 있도록 정하는 것이 좋다.
mappedBy를 통해서 어떤 객체가 주인인지 표시할 수 있다.
양방향 매핑을 하고 toString()을 실행하는 시점에서 순환참조가 발생해, StackOverFlowError가 발생할 수 있다. 따라서 순환참조를 제거하기 위해서 exclude를 사용해 ToString에서 제외 설정하는 것이 좋다.
상품(상품번호, 상품이름, 상품가격, 상품재고, 공급업체번호, 상품분류번호, 상품생성일자, 상품정보변경일자) 테이블과 공급업체(공급업체번호, 업체이름, 업체생성일자, 업체정보변경일자) 테이블의 관계를 생각해보면 일대다 관계로 볼 수 있다.
Product 클래스가 외래키를 갖는 예시를 보고 알아보겠다. 일반적으로 외래키를 갖는 쪽이 주인의 역할을 수행한다는 것을 기억하자.
공급업체에 매핑되는 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;
}
상품 엔티티는 다음과 같다.
@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;
}
리포지토리를 생성하면 엔티티를 활용할 수 있게 된다.
@Repository("productRepositorySupport")
public interface ProductRepository extends JpaRepository<Product, Long>, ProductRepositoryCustom {
}
이 경우에는 ProductRepository를 통한 조회만 가능하다. 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;
@OneToMany(mappedBy = "provider", cascade = CascadeType.PERSIST, orphanRemoval = true)
@ToString.Exclude
private List<Product> productList = new ArrayList<>();
}
Product가 여러개가 될 수도 있으므로 컬렉션(Collection, List, Map) 형식으로 필드를 생성한다.
생략..