[7/26 TIL] JPA(연관관계매핑, 고급매핑, 프록시 객체)

yumyeonghan·2023년 7월 27일
0

🍃프로그래머스 백엔드 데브코스 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") // fk
    private String order_id;
    @Column(name = "item_id") // fk
    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") // 연관관계 주인은 order.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
profile
웹 개발에 관심 있습니다.

0개의 댓글