🍃프로그래머스 백엔드 데브코스 4기 교육과정을 듣고 정리한 글입니다.🍃
연관관계매핑
데이터 중심 설계의 문제점
@Entity
@Table(name = "order_item")
@Getter
@Setter
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private int price;
private int quantity;
@Column(name = "order_id")
private String order_id;
@Column(name = "item_id")
private Long item_id;
}
- 이런식으로 설계하면 ERD 형태로 테이블이 생성되지만, 실제 엔티티 객체 사이에서는 서로 참조를 못한다.
- JPA는 객체 중심으로 설계할 수 있게 도와준다.
객체 연관관계 vs 테이블 연관관계
- 테이블은 외래키로 연관 관계를 맺는다.
- 객체는 참조로 연관관계를 맺는다.
방향 (단방향, 양방향)
- ERD 테이블에서는 외래키를 이용해서 양방향으로 조인이 가능하다.
- 객체에서는 한쪽만 참조하는 단방향, 양쪽 모두 서로 참조하는 양방향 관계가 있다.
다중성 (다대일, 일대다, 다대다, 일대일)
- 회원과 주문은 일대다(@OneToMany)
- 주문과 회원은 다대일(@ManyToOne)
연관관계 주인 (mappedBy)
- 객체를 양방향 관계로 만들면, 연관관계 주인을 지정해야한다.
- 연관관계 주인만이, 외래키를 등록, 수정, 삭제를 할 수 있다.
- 주인이 아닌쪽은 읽기만 가능하다.
- ERD 테이블중 FK가 있는쪽이 연관관계 주인이 된다.
양방향 연관관계 예시
@Entity
@Table(name = "member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(name = "name", nullable = false, length = 30)
private String name;
@Column(nullable = false, length = 30, unique = true)
private String nickName;
private int age;
@Column(name = "address", nullable = false)
private String address;
@Column(name = "description", nullable = true)
private String description;
@OneToMany(mappedBy = "member")
private List<Order> orders;
}
- 양방향 관계를 맺기 때문에 mappedBy를 사용해서 연결함
- 단방향 관계라면 mappedBy을 사용하는 필드 자체가 필요없음
@Entity
@Table(name = "orders")
public class Order {
@Id
@Column(name = "id")
private String uuid;
@Column(name = "order_datetime", columnDefinition = "TIMESTAMP")
private LocalDateTime orderDatetime;
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus;
@Lob
private String memo;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", referencedColumnName = "id")
private Member member;
public void setMember(Member member) {
if(Objects.nonNull(this.member)) {
this.member.getOrders().remove(this);
}
this.member = member;
member.getOrders().add(this);
}
}
- 양방향 연관관계의 주인이기 때문에 외래키를 관리함
- 연관관계 편의 메소드를 두어 외래키를 설정하고, 주인이 아닌 객체에도 RDB 테이블과 관련 없는 데이터를 추가함
고급매핑
상속관계매핑(싱글테이블 전략)
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
}
@Entity
@DiscriminatorValue("FOOD")
public class Food extends Item{
private String chef;
}
@Entity
@DiscriminatorValue("CAR")
public class Car extends Item{
private long power;
}
- item 테이블에 이를 상속받은 Food와 Car의 속성과 함께 @DiscriminatorColumn으로 지정한 DTYPE 컬럼이 생성된다.
- DTYPE 컬럼에는 @DiscriminatorValue 지정한 값이 들어간다.
- 싱글 테이블이므로 하위 타입의 컬럼에는 null 값이 들어갈 수 있어야 한다.
- 객체 중심 설계
상속관계매핑(조인테이블 전략)
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Item {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
}
@Entity
public class Food extends Item {
private String chef;
}
@Entity
public class Car extends Item {
private long power;
}
- 테이블 생성시 item 테이블과 하위 테이블이 외래키 기반으로 조인되게끔 자동으로 생성된다.
- 즉 테이블을 각각 만든다.
- 하위 테이블의 pk값은 item의 id(PK,FK)로 구성된다.
- 데이터 중심 설계
@MappedSuperclass
@MappedSuperclass
public class BaseEntity {
@Column(name = "created_by")
private String createdBy;
@Column(name = "created_at", columnDefinition = "TIMESTAMP")
private LocalDateTime cratedAt;
}
@Entity
@Table(name = "orders")
public class Order extends BaseEntity {
@Id
private String uuid;
}
- 실제 엔티티 즉, ERD 테이블에 생성되지 않지만, @MappedSuperclass 객체를 상속 받은 엔티티는 해당 객체의 필드까지 ERD 테이블에 생성된다.
- orders 테이블에 "created_by", "created_at" 컬럼이 추가된다.
@EmbeddedId
@Entity
public class Parent {
@EmbeddedId
private ParentId id;
}
@EqualsAndHashCode
@NoArgsConstructor
@Embeddable
public class ParentId implements Serializable {
private String id1;
private String id2;
}
- 복합키로 pk값을 설정할 수 있다. (복합키 매핑)
- Serializable 인터페이스를 구현해야 한다.
- eqauls, hashCode를 구현해야 한다.
- 기본 생성자가 있어야 한다.
- 식별자 클래스는 public이어야 한다.
- @Embeddable이 있어야 한다.
프록시 객체
@Entity
@Table(name = "orders")
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", referencedColumnName = "id")
private Member member;
}
- JPA는 프록시 객체를 사용해서 연관된 객체를 처음부터 데이터베이스에서 조회하지 않고, 실제 사용하는 시점에 조회할 수 있다.
- 이를 지연로딩이라 한다. (FetchType.LAZY)
- order를 조회하면 member가 프록시 객체로 초기화된다.
- 이 프록시 member 객체를 통해 실제 사용할 때 member 테이블에 대한 조회 쿼리를 수행한다.
- order.getMember().getName();
- 이러한 과정은 영속성 컨텍스트의 도움을 받아야 가능하다.
- 따라서 준영속 상태에서 프록시를 사용하면, LazyInitializationException 예외가 발생한다.
지연로딩, 즉시로딩
- 지연로딩은 엔티티를 조회할 때, 연관된 엔티티를 함께 조인을 통해 조회한다.
- 즉시로딩은 연관된 엔티티를 실제 사용할 때 조회한다.
영속성 전이 (CASCADE)
@Entity
@Table(name = "orders")
public class Order {
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "member_id", referencedColumnName = "id")
private Member member;
}
- 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 함께 영속상태로 만든다.
- em.persist(order)
- em.persist(member)을 하지 않아도 member가 영속상태가 된다.
- CascadeType에는 ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH가 있다.
고아 객체
@Entity
public class Member {
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
}
- 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제한다.
- member.getOrders().remove(0)
- 해당 order 엔티티가 삭제되고, delete 쿼리를 수행한다.
- orphanRemoval = true