JPA에서의연관관계Mapping :
。JPA에서DB Entity에서 다른연관관계에 있는DB Entity 객체를 가져올 때어노테이션을 통해연관관계를 설정.
▶RDBMS는 단순히FK만 설정하면 자동으로양방향 관계가 되므로Join을 수행하면 원하는 Data를 가져올 수 있으나JPA는DB Entity간양방향 관계를 맺어야Entity간에Entity객체를 가져올 수 있다.
。구현하고자 하는서비스에 따라서 각Entity간단방향 / 양방향 관계여부파악 후연관관계설정이 필요
。연관관계에서부모 Entity를 기준으로Mapping을 수행하며자식 Entity는FK가 존재하는Entity쪽으로 설정
▶ 일반적으로부모 Entity와자식 Entity간 관계는1:N관계로서부모 Entity는 여러자식 Entity 객체를 가질 수 있고자식 Entity는 하나의부모 Entity 객체를 가진다.
▶RDBMS와 동일하게연관관계에서1:N인 경우N은FK(외래키)가 있는쪽으로 설정
。특정Entity의field에 존재하는연관관계의Entity 객체를 조회하는건추가질의를 하는것과 동일
▶ 불필요한Entity 객체까지 포함한연관관계의 모든Entity 객체를 조회하는건 성능 상 문제가 발생할 수 있으므로(fetch = FetchType.LAZY)등의 전략을 잘 설정해야한다
。M:N 관계에서는 사이에 추가로Mapping Table을 구현하여 각Entity와Mapping Table Entity간1:N 관계구축
Entity간연관관계종류
단방향 관계:
。단방향으로의DB Entity만 참조가 가능한 관계
▶@OneToOne의 경우자식 Entity에서부모 Entity를 참조할 수 있음.
ex)Member Entity에서Team Entity의 정보를 가져올 수 있지만, 반대는 불가능
양방향 관계:
。양 쪽의DB Entity가 서로 참조가 가능한 관계
▶@OneToMany&@ManyToOne의 경우부모Entity와자식 Entity에서 서로 참조가능
ex)Member Entity와Team Entity간 에서 서로의Entity를 참조가능
DB Entity간연관관계시 주의사항
DB Entity가영속화되기전인비영속 상태인 경우POJO와 차이가 없다.@OneToMany(mappedBy="member", fetch = FetchType.EAGER) private List<OrderEntity> orders = new ArrayList<>();。다음처럼
DB Entity Class의연관관계를 정의하는field가 정의되어 있을때, 해당field는DB Entity 객체가save(DBEntity객체)를 통해영속성 컨텍스트에 등록되어영속화된 시점에서DB에Query를 전달하여연관관계의초기화를 수행한다.
▶FetchType.EAGER의 경우DB에서List<OrderEntity>를 가져와서초기화
▶FetchType.LAZY의 경우Hibernate에 의해프록시객체를 생성하여초기화
。이때 해당DB Entity가 가 수행되지않아영속화되기전인비영속 상태인 경우POJO로서 해당field는영속화될때까지 비어있는new ArrayList<>()상태로 존재
▶ 이러한 다른Entity와의존성이 존재하지 않는 상태의비영속 상태인 경우단위테스트가 가능
부모 Entity가 먼저영속성 컨텍스트에 등록된 후자식 Entity를영속성 컨텍스트에 등록// 자식 Entity 이므로 외래키 Field에 @ManyToOne을 선언 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name="memberId") // DB Table의 외래키 field명 설정 private MemberEntity member;。
부모 Entity가EntityManager.save()를 통해영속성 컨텍스트에 등록되기 전자식 Entity가 등록될 경우자식 Entity Class내FK Field의부모 Entity 객체가 정의되지 않으므로 오류가 발생한다.
▶ 반드시부모 Entity를 먼저 등록
JPA의DB Entity간연관관계Mapping 용도의JPA 어노테이션종류 :
。JPA(jakarta.persistence)에 포함
。@Entity를 선언한Entity Class의외래키용도의Field에 선언하여DB Entity간연관관계를 설정
。@OneToOne, @OneToMany , @ManyToOne, @ManyToMany
▶@OneToMany , @ManyToOne를 주로 사용
@OneToOne
。Entity간1:1 관계의연관관계를 정의
。자식Entity의부모 Entity data type의Field에 선언
▶자식Entity가FK Column을 관리
。부모 Entity에서는@OneToOne(mappedBy="자식테이블연관필드명")으로 설정
▶cascade = CascadeType.ALL등의제약조건을 추가 설정 가능// 자식 Entity @OneToOne @JoinColumn(name = "order_product_id", nullable = false) private OrderProductEntity orderProduct;
@OneToMany(mappedBy="자식Entity의 @ManyToOne필드명")
。DB Entity간양방향 관계설정 시부모 Entity의Field에 선언하는어노테이션
▶1:N관계를Mapping할 때 다수의자식 Entity들을 저장하는List<자식EntityClass>data type의field에 선언
。부모 Entity에서@OneToMany를 선언하여연관관계 부모를 지정하여양방향 Mapping시부모 Entity에서FK Column을중복생성하지 않도록 방지
▶FK는자식 Entity에서만 가져야 하므로
。(fetch = FetchType.LAZY)가 기본값으로 설정됨
▶부모 Entity생성 시점에서연관관계의자식 Entity를DB에서 가져오지않고Hibernate에 의해프록시 객체로 초기화
(cascade = CascadeType.ALL, orphanRemoval = true)
。부모 Entity가 삭제된 경우 연관된자식 Entity도 함께 삭제하도록JPA 레벨에서제약조건설정
▶ORM에서만 적용되며 실제DB Table에서는 적용되지 않음.
。orphanRemoval = true를 추가설정하여부모 Entity가자식 Entity를 더 이상 참조하지않을때고아가 된자식 Entity자동으로 삭제
(mappedBy="자식Entity의 @ManyToOne필드명")를 설정하는 이유?
。@OneToMany만 선언하는 경우M:N 관계라고 판단하여JPA에 의해 자동으로Mapping Table이 생성
▶ 해당 경우를 방지하기 위해1:N 관계인 경우자식 Entity의@ManyToOne + @JoinColumn 필드명으로 설정 시Mapping Table을 생성하지 않는다.
@ManyToOne
。DB Entity간양방향 관계로서1:N관계를 설정 시자식 Entity에서 Mapping할 때부모 Entity 객체를 저장하는field에 선언하는어노테이션
▶ 주로@JoinColumn과 함께 사용
。외래키의관리주체를 결정하며 선언된Entity의DB Table에FK Field가 생성됨
▶자식 Entity가Mapping한자식테이블에외래키 Column이 생성되므로
。(fetch = FetchType.EAGER)가 기본값으로 설정됨
▶자식 Entity생성 시점에서연관관계의부모 Entity도 전부DB에서 가져와서 생성
(optional = false)
。해당FK 필드는NOT NULL로 설정
@JoinColumn(name = "FK필드명"):
。@OneToOne,@ManyToOne을 통한연관관계정의 시자식Entity쪽에서매핑한DB Table에 생성될FK의Column명을 지정하는어노테이션
▶1:1또는1:N연관관계설정 시자식 Entity의@OneToOne,@ManyToOne이 선언된외래키 Field에서 추가로 선언
@JoinColumn예시@ManyToOne @JoinColumn( name = "route_id", nullable = false, foreignKey = @ForeignKey( name = "fk_routelink_route", foreignKeyDefinition = "FOREIGN KEY(route_id) REFERENCES route(id) ON DELETE CASCADE" ) ) private Route route;。 다음의 경우
@ForeignKey를 통해 설정된FK로Order Entity의DB Table에서user_id라는 명칭으로FK Field가 생성
▶영속성 컨텍스트를 통해Entity를 삭제하는 경우가 아닌JPQL또는Native SQL을 통해 삭제를 수행 시 다음처럼@ForeignKey를 통해DB Table에 직접제약조건을 설정.
▶영속성 컨텍스트를 통해 삭제 시@OneToMany(cascade = CascadeType.ALL,orphanRemoval = true)를 정의
@JoinColumn( nullable = false )
。선언된FK Field를NOT NULL 속성으로DB TABLE에 설정
@JoinColumn( foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT) )
。Entity에매핑된DB Table생성 시FK 제약조건을 생성하지 않도록 설정
@ManyToOne,@OneToMany에서연관관계의Entity의초기화 전략
。연관관계 Entity 객체의lazily loaded/eagerly fetched의 여부를 결정
。연관관계 Entity가 필요한 경우FetchType.EAGER를 사용하고, 필요없을 경우FetchType.LAZY를 사용
(fetch = FetchType.LAZY)
。@OneToMany의default 속성
。특정Entity를DB에서조회하는 시점에서연관관계의Entity는조회하지않고Hibernate가 생성한프록시 객체로 대체하는 방식
▶ 해당Entity 객체에서연관관계 Entity객체로 접근하는 시점에서DB에Query를 전송하여 해당연관관계 Entity객체를 가져와서 할당
。EAGER 방식에 비해 불필요한Entity까지 조회하지 않으므로성능문제가 크게 발생하지 않아 실무에서 주로사용
。이때연관관계의Entity를 추가 조회하는 시점에서반복조회가 발생 시N+1 문제가 발생할 수 있음
▶연관관계 Entity가 필요한 경우FetchType.EAGER를 선언
(fetch = FetchType.EAGER)
。@ManyToOne의default 속성
。특정Entity를DB에서조회하는 시점에서연관관계의Entity도 함께 즉시조회하여 가져오는 방식
。불필요한Entity까지 모두 조회하며연관관계가 복잡하면 성능문제가 발생
▶연관관계 Entity가 필요없는 경우FetchType.LAZY사용
1:1 연관관계구현 예시
。자식 Entity는부모 Entity의FK Column을 가진다.
。자식 Entity Class에서만부모 Entity Data Type을 참조하는Field에@OneToOne을 선언@Getter @Entity(name = "review") @NoArgsConstructor(access = PROTECTED) public class ReviewEntity extends BaseEntity { @Column(nullable = false) @Enumerated(EnumType.STRING) private ReviewStatus status; @Column(nullable = false) private String content; // 자식Entity 클래스에서 1:1 관계 정의 @OneToOne @JoinColumn(name = "order_product_id", nullable = false) private OrderProductEntity orderProduct; public void update(String content){ this.content = content; } public void delete(){ this.status = ReviewStatus.REMOVED; } protected ReviewEntity(String content, ReviewStatus status) { ValidationUtil.validateNotNullAndBlank(content,"내용"); this.status = status; this.content = content; } public static ReviewEntity create(final String content){ return new ReviewEntity(content, ReviewStatus.ENABLED); } public void mapToOrderProduct(OrderProductEntity orderProduct){ this.orderProduct = orderProduct; } }
1:N 연관관계구현 예시
- 부모
Entity
。Member와Order의연관관계 = 1:N
▶Member는부모 Entity이므로 여러자식 Entity 객체가 정의된List<OrderEntity> Field에@OneToMany선언@Getter @Entity @Table(name="Member") public class MemberEntity extends BaseEntity { // 부모 Entity이므로 여러 자식 Entity 객체를 가진다 @OneToMany(mappedBy="member") private List<OrderEntity> orders = new ArrayList<>(); // 자식 Entity객체가 생성 시 부모 Entity의 List 필드에 넣는 // 매핑용 메서드 public void mapToOrder(OrderEntity order){ orders.add(order); } }。
@OneToMany는default로(fetch = FetchType.LAZY)이므로MemberEntity가 생성되더라도List<OrderEntity>는DB에서 가져오지않고프록시 객체로 채워짐
▶List<OrderEntity>를 사용하는 시점에서DB에서 해당OrderEntity객체를 가져와서orders로 할당
주의사항 :
mapTo??() 메서드
자식Entity객체가 생성 된 경우양방향관계의부모 Entity의@OneToMany List<> 필드에도 생성된Entity 객체를 추가해야한다.
。해당mapping용메서드(mapTo??())를부모Entity내 구현하여 추후Mapping Table용도의Entity 객체가 새로 생성된 경우 해당메서드를 통해 반영// 매핑용 메서드 public void mapToOrder(OrderEntity order){ orders.add(order); }
자식 Entity 객체를 생성 시 해당Entity객체내부의static factory method를 통해Entity객체를 생성하면서부모 Entity의매핑용메서드를 호출하여 전달// 자식 Entity 이므로 외래키 Field에 @ManyToOne을 선언 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name="memberId") // DB Table의 외래키 field명 설정 private MemberEntity member; private OrderEntity(Receiver receiver, LocalDateTime deliveredAt, MemberEntity member, OrderStatus orderStatus) { this.receiver = receiver; this.deliveredAt = deliveredAt; this.member = member; this.orderStatus = orderStatus; } // 정적 팩토리 메서드 public static OrderEntity of(Receiver receiver, MemberEntity member){ var order = new OrderEntity( receiver, LocalDateTime.now().plusDays(3), member, OrderStatus.PENDING ); member.mapToOrder(order); // 부모 Entity 객체에 생성된 OrderEntity객체 추가 return order; }▶
생성자는캡슐화되어있고정적 팩토리 메서드에 의해객체 생성이 대리로 수행되며 해당메서드내에서 생성된OrderEntity 객체를OrderEntity객체내@ManyToOne으로 선언된부모 Entity의@OneToMany List<> 필드에 추가
- 자식
Entity
。Order는자식 Entity이므로 단일부모 Entity 객체가 정의된MemberEntity Field에@ManyToOne과@JoinColumn선언@Entity @Getter @Table(name = "Orders") public class OrderEntity extends BaseEntity { // 자식 Entity 이므로 외래키 Field에 @ManyToOne을 선언 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name="memberId") // DB Table의 외래키 field명 설정 private MemberEntity member; private OrderEntity(Receiver receiver, LocalDateTime deliveredAt, MemberEntity member, OrderStatus orderStatus) { this.receiver = receiver; this.deliveredAt = deliveredAt; this.member = member; this.orderStatus = orderStatus; } // 정적 팩토리 메서드 public static OrderEntity of(Receiver receiver, MemberEntity member){ var order = new OrderEntity( receiver, LocalDateTime.now().plusDays(3), member, OrderStatus.PENDING ); member.mapToOrder(order); // 부모 Entity 객체에 생성된 OrderEntity객체 추가 return order; } }。
@ManyToOne는default로(fetch = FetchType.EAGER)이므로OrderEntity가 생성 시MemberEntity도 함께DB에서 가져와서 할당
▶(fetch = FetchType.LAZY)로 따로 설정가능
。 이후자식 Table에부모 Table을참조하는FK Field가 생성됨을 확인가능
M:N 연관관계구현 예시
。Order는 여러개의Product를 가질수 있다.
。Product는 여러개의Order를 가질 수 있다.
▶ 두DB Entity는 서로M:N관계를 만족하므로 중간에Mapping Table역할의OrderProductEntity를 정의
。OrderEntity와ProductEntity는Mapping Table에 대해1:N 관계이므로List<OrderProductEntity> 객체를 가지는@OneToMany이 선언된Field를 정의
。OrderProductEntity는 각각OrderEntity와ProductEntity를 참조하는 단일Entity 객체를 가지는@ManyToOne + @JoinColumn이 선언된field를 정의
Mapping Table역할의DB Entity정의
。Mapping Table이므로 양쪽부모 Entity를 각각 참조하는fk field선언
▶@ManyToOne + @JoinColumn으로 선언한다.@Entity @Getter @NoArgsConstructor @Table(name="OrderProduct") public class OrderProductEntity extends BaseEntity { private Long quantity; // Mapping Table이므로 양쪽 Table을 1:1로 참조하는 field를 갖는다. @ManyToOne @JoinColumn(name = "order_id") private OrderEntity order; @ManyToOne @JoinColumn(name = "product_id") private ProductEntity product; private OrderProductEntity(Long quantity, OrderEntity order, ProductEntity product) { this.quantity = quantity; this.order = order; this.product = product; } public static OrderProductEntity of(Long quantity, OrderEntity order, ProductEntity product){ OrderProductEntity orderProduct = new OrderProductEntity(quantity,order,product); // OrderProduct 생성 시 각각 부모Entity인 ProductEntity , OrderEntity에 반영 order.mapToOrderProduct(orderProduct); product.mapToOrderProduct(orderProduct); return orderProduct; } }
Mapping Table기준 양쪽부모 Entity정의
。OrderEntity와ProductEntity는Mapping Table에 대해1:N 관계이므로List<OrderProductEntity> 객체를 가지는@OneToMany이 선언된Field를 정의
▶ 각각부모 Entity이므로OrderProductEntity객체생성 시 해당@OneToMany 필드객체로 추가하는메서드도 구현@Getter @Entity @NoArgsConstructor @Table(name= "Product") public class ProductEntity extends BaseEntity { private String name; private Long price; private Long stock; @Enumerated(EnumType.STRING) private ProductStatus status; // Mapping Table 역할의 Entity와 1:N 관계에서 부모 @OneToMany(mappedBy="product") private List<OrderProductEntity> orderProductEntities = new ArrayList<>(); // OrderProduct 생성 시 수작업으로 양방향 관계에 있는 Entity의 // @OneToMany가 선언된 List 필드에 추가해야한다. public void mapToOrderProduct(OrderProductEntity orderProductEntity){ orderProductEntities.add(orderProductEntity); } }@Entity @Getter @Table(name = "Orders") public class OrderEntity extends BaseEntity { @Embedded private Receiver receiver; @Enumerated(EnumType.STRING) private OrderStatus orderStatus; private LocalDateTime deliveredAt; // Mapping Table 역할의 Entity와 1:N 관계에서 부모 @OneToMany(mappedBy = "order") List<OrderProductEntity> orderProductEntities = new ArrayList<>(); // OrderProduct 생성 시 수작업으로 양방향 관계에 있는 Entity의 // @OneToMany가 선언된 List 필드에 추가해야한다. public void mapToOrderProduct(OrderProductEntity orderProductEntity){ orderProductEntities.add(orderProductEntity); } }
▶Mapping Table에 각각부모인DB Table을 참조하는FK Field가 생성
주의사항 :
OrderProductEntity가 생성 된 경우양방향관계의부모 Entity의@OneToMany List<> 필드에도 생성된Entity 객체를 추가해야한다.
。해당mapping용메서드를부모Entity내 구현하여 추후Mapping Table용도의Entity 객체가 새로 생성된 경우 해당메서드를 통해 반영// 부모 Entity 내 반영 public void mapToOrderProduct(OrderProductEntity orderProductEntity){ orderProductEntities.add(orderProductEntity); }
자식 Entity 객체를 생성 시 해당Entity객체내부의static factory method를 통해Entity객체를 생성하면서부모 Entity의매핑용메서드를 호출하여 전달@ManyToOne @JoinColumn(name = "order_id") private OrderEntity order; @ManyToOne @JoinColumn(name = "product_id") private ProductEntity product; private OrderProductEntity(Long quantity, OrderEntity order, ProductEntity product) { this.quantity = quantity; this.order = order; this.product = product; } public static OrderProductEntity of(Long quantity, OrderEntity order, ProductEntity product){ OrderProductEntity orderProduct = new OrderProductEntity(quantity,order,product); // OrderProduct 생성 시 각각 부모Entity인 ProductEntity , OrderEntity에 반영 order.mapToOrderProduct(orderProduct); product.mapToOrderProduct(orderProduct); return orderProduct; }▶
OrderProduct 객체생성 시연관관계의 양쪽부모 Entity의List 필드에도 추가하는메서드실행
@ManyToManyN:M:
。실무에서는1:NorN:1을 통해 중간에Mapping Table을 구현하여 처리하므로 사용하지 않는다.