소스
https://github.com/rkwhr0010/jpa
상속 관계 매핑
RDB는 상속이라는 개념이 없다.
모델링 관점에서 슈퍼타입 서브타입 관계가 있다.
ORM에서 상속 관계 매핑은 객체의 상속 구조와 DB 슈퍼타입 서브타입 관계를 매핑하는 것을 의미한다.
세 가지 전략
- 각각의 테이블로 변환
부모 테이블, 자식 테이블 외래 키로 조인
JPA에서 조인 전략이라 한다. - 통합 테이블로 변환
하나의 테이블로 객체 상속 관계 모든 필드를 컬럼으로 만든다.
JPA 단일 테이블 전략 - 서브타입 테이블로 변환
부모 타입 테이블은 안만들고 대신, 부모 타입 속성을 서브 타입 마다 가지고 있음
구현 클래스 마다 테이블 전략
조인 전략
부모 테이블, 자식 테이블을 만들고 사이에 외래 키로 조인한다.
자식 테이블은 부모의 개인 키를 받아 외래 키 + 기본 키로 사용하는 식별관계가 된다.
객체에선 타입이라는 개념이 존재해 부모 타입, 서브 타입을 구별가능하지만, 테이블엔 그런게 없다. 따라서 타입 구분 컬럼이 필요하다.
@Entity @Inheritance(strategy = InheritanceType.JOINED) @DiscriminatorColumn(name = "DTYPE") // 기본 값이 DTYPE이다. @Data public abstract class Item { @Id @GeneratedValue @Column(name = "item_id") private Long id; private String name; private int price; } |
@Entity @DiscriminatorValue("A") @Data public class Album extends Item { private String artist; } |
@Entity @DiscriminatorValue("M") @Data public class Movie extends Item { private String director; private String actor; } |
@Entity @DiscriminatorValue("B") @PrimaryKeyJoinColumn(name = "book_id") @Data public class Book extends Item{ private String author; private String isbn; } |
상속 매핑은 부모 클래스에 해야 한다. @Inheritance 어노테이션으로 설정한다.
전략은 세 가지
@DiscriminatorColumn 어노테이션은 구분 컬럼을 설정한다.
이 값으로 자식 타입 테이블을 식별한다.
@DiscriminatorValue 어노테이션으로 자식 엔티티가 어떤 구분 값을 가질 지 설정한다.
@PrimaryKeyJoinColumn 어노테이션으로 자식 테이블의 기본 키 컬럼 이름을 변경할 수 있다.
장점
- 테이블 정규화
- 외래 키 참조 무결성 제약 조건 활용
- 저장공간 효율적
단점
- 조회 시 조인 사용으로 성능 저하 가능성
- 데이터 저장 시 INSERT 2번
특징
JPA 표준 명세엔 구분 컬럼이 필수지만, 하이버네이트 포함 몇몇 구현체는 구분 컬럼 없이도 동작한다.
단점으로 소개된 결과는 다 테이블이 쪼개진(정규화된) 결과로 파생되는 증상들이다.
private static void delete() { logic(em -> { Book book = em.find(Book.class, 1L); em.remove(book); }); } private static void findAndUpdate() { logic(em -> { Book book = em.find(Book.class, 1L); book.setIsbn("2222222"); book.setAuthor("현진건"); book.setName("운수좋은날"); }); } private static void save() { logic(em -> { Book book = new Book(); book.setAuthor("윤흥길"); book.setIsbn("111111"); book.setPrice(5000); book.setName("아홉켤레의구두로남은사내"); em.persist(book); }); } |
단일 테이블 전략
@Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "DTYPE") // 기본 값이 DTYPE이다. @Data public abstract class Item { @Id @GeneratedValue @Column(name = "item_id") private Long id; private String name; private int price; } |
장점
- 조인이 없어 조회가 편리하고, 빠르다
단점
- 자식 엔티티가 매핑한 컬럼은 무조건 null 허용해야 한다.
논리적으로 자식 엔티티들로 행에 모두 데이터를 채울 수 없음 - 단일 테이블이 커질 수 있다.
특징
몇몇 구현체는 구분 컬럼이 없어도 동작 가능하다고 헀지만, 이 전략은 구분 컬럼이 필수다. @DiscriminatorColumn 필수, 자식 엔티티에 @DiscriminatorValue 가 없으면, 기본 값으로 자식 엔티티 이름을 사용한다.
구현 클래스마다 테이블 전략
@Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) @Data public abstract class Item { @Id @GeneratedValue @Column(name = "item_id") private Long id; private String name; private int price; } |
@Entity @Data public class Album extends Item { private String artist; } |
@Entity @Data public class Book extends Item{ private String author; private String isbn; } |
@Entity @Data public class Movie extends Item { private String director; private String actor; } |
일반적으로 추천하지 않는 전략, 사용하지 말 것
장점
- 서브 타입 구분이 명확
- not null 제약 조건 가능
단점
- 여러 자식 테이블을 함께 조회 시 느리다. UNION 으로 행을 붙여야 한다.
자식 테이블 통합 쿼리가 어렵다.
일반 조인을 사용하면 컬럼으로 붙인다.
특징
- 구분 컬럼이 없다.
@MappedSuperclass
부모 클래스 속성을 사용하되, 부모 엔티티를 테이블에 매핑하고 싶지 않을 때 사용한다.
상속 관계를 표현하는 목적이 아닌 단순히 공통의 매핑 정보만 제공할 목적으로 사용한다.
@MappedSuperclass @Data public abstract class BaseEntity { @Id @GeneratedValue private Long id; private LocalDateTime reg_date; // 등록일자 private String reg_id; //등록자 ID private LocalDateTime mod_date; // 수정일자 private String mod_id; // 수정자 ID } |
@Entity @Data public class Board extends BaseEntity { private String name; private String contents; } |
@Entity @AttributeOverrides({ @AttributeOverride(name = "id", column = @Column(name = "member_id")) }) @Data public class Member extends BaseEntity{ private String name; } |
@AttributeOverride 어노테이션을 통해 상속받은 매핑 정보를 재정의할 수 있다.
@MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 영속성 컨텍스트에서 사용할 수 없다.
보통 인스턴스를 생성하지 못하도록 추상 클래스로 선언한다.
@Entity는 원래 @Entity인 클래스만 상속 받을 수 있지만, @MappedSuperclass도 상속 받을 수 있다.
복합 키와 식별 관계 매핑
DB 테이블 사이 외래 키 관계
- 식별 관계
외래 키를 자신의 기본 키로 사용
이때 복합 키로 사용되어도 식별 관계이다. - 비식별 관계
외래 키로만 사용
null 허용 여부에 따라 추가 분류
보통 비식별 관계를 사용하고, 꼭 필요한 곳만 식별 관계를 사용한다.
복합키 : 비식별 관계 매핑
방법 두 가지
- @IdClass
DB에 가까운 방법 - @EmbeddedId
객체지향에 가까운 방법
@IdClass
@Entity @IdClass(MasterId.class) @Data public class Master { @Id @GeneratedValue @Column(name = "master_id1") private Long id1; @Id @GeneratedValue @Column(name = "master_id2") private Long id2; private String name; } |
@EqualsAndHashCode @AllArgsConstructor @NoArgsConstructor public class MasterId implements Serializable{ private Long id1; private Long id2; } |
@Entity @Data public class Detail { @Id @GeneratedValue @Column(name = "detail_id") private Long id; @ManyToOne @JoinColumns({ @JoinColumn(name = "fk_master_id1", referencedColumnName = "master_id1"), @JoinColumn(name = "fk_master_id2", referencedColumnName = "master_id2") }) private Master master; } |
@IdClass 식별자 클래스 조건
- 속성명이 같아야 한다.
- Serializable 구현
- equals, hashCode 재정의
- 기본 생성자 필수
- public 클래스 일 것
@Entity @IdClass(Master.MasterId.class) @Data public class Master { @Id @GeneratedValue @Column(name = "master_id1") private Long id1; @Id @GeneratedValue @Column(name = "master_id2") private Long id2; private String name; @EqualsAndHashCode @AllArgsConstructor @NoArgsConstructor public static class MasterId implements Serializable{ private Long id1; private Long id2; } } |
참고로 내장 클래스로 사용해도 된다.
private static void find() { logic(em -> { Master.MasterId masterId = new Master.MasterId(1L, 2L); em.find(Master.class, masterId); em.find(Detail.class, 1L); }); } private static void save() { logic(em -> { Master master = new Master(); master.setName("마스터"); em.persist(master); Detail detail = new Detail(); detail.setMaster(master); em.persist(detail); }); } |
@EmbeddedId
객체지향적 방법
@Entity @Data public class Master { @EmbeddedId private Master.MasterId id; private String name; @EqualsAndHashCode @AllArgsConstructor @NoArgsConstructor @Embeddable public static class MasterId implements Serializable{ @Column(name = "master_id1") private Long id1; @Column(name = "master_id2") private Long id2; } } |
@Entity @Data public class Detail { @Id @GeneratedValue @Column(name = "detail_id") private Long id; @ManyToOne @JoinColumns({ @JoinColumn(name = "fk_master_id1", referencedColumnName = "master_id1"), @JoinColumn(name = "fk_master_id2", referencedColumnName = "master_id2") }) private Master master; } |
private static void find() { logic(em -> { Master.MasterId masterId = new Master.MasterId(1L, 2L); em.find(Master.class, masterId); em.find(Detail.class, 1L); }); } private static void save() { logic(em -> { Master master = new Master(); master.setId(new Master.MasterId(1L, 2L)); master.setName("마스터"); em.persist(master); Detail detail = new Detail(); detail.setMaster(master); em.persist(detail); }); } |
@GeneratedValue 사용이 불가능해서 제거 후 직접 키를 할당했다.
DB는 전혀 바뀌는 게 없다. 단지 애플리케이션에서 매핑을 어떻게 하냐만 다를 뿐
식별자 클래스에 @Embeddable 애노테이션을 붙인다.
식별자 클래스에 직접 기본 키를 매핑한다.
식별자 클래스를 직접 필드로 넣고 @EmbeddedId 애노테이션을 붙인다.
식별자 클래스 조건
- @Embeddable 붙임
- Serializable 구현
- equals, hashCode 재정의
- 기본 생성자 필수
- public 클래스일 것
영속성 컨텍스트는 식별자를 기반으로 엔티티를 관리한다.
식별자를 비교할 때 equals, hashCode를 사용하므로
식별자 클래스는 반드시 재정의해야 한다.
@IdClass vs @EmbeddedId
각 장단점이 존재하므로, 중요한 것은 하나를 선택하면 일관성 있게 사용해야 한다.
복합 키 식별 관계 매핑
@IdClass 식별 관계
@Entity @Data public class A { @Id @Column(name = "a_id") private String id; private String name;; } |
@Entity @IdClass(B.BId.class) @Data public class B { @Id @ManyToOne @JoinColumn(name = "a_id") // 외래 키 식별 private A a; @Id @Column(name = "b_id") private String id; private String name;; @NoArgsConstructor @EqualsAndHashCode @Data public static class BId implements Serializable { private String a; //public A a; private String id; //private String id; } } |
@Entity @IdClass(C.CId.class) @Data public class C { @Id @ManyToOne @JoinColumns({ @JoinColumn(name = "a_id"), @JoinColumn(name = "b_id") }) private B b; @Id @Column(name = "c_id") private String id; private String name; @NoArgsConstructor @EqualsAndHashCode @Data public static class CId implements Serializable{ private B.BId b; //private B b; private String id; // private String id; } } |
public static void main(String[] args) { save(); find(); emf.close(); } private static void find() { logic(em -> { A a = em.find(A.class, "A_ID"); BId bId = new B.BId(); bId.setA("A_ID"); bId.setId("B_ID"); B b = em.find(B.class, bId); CId cId = new C.CId(); cId.setB(bId); cId.setId("C_ID"); C c = em.find(C.class, cId); }); } private static void save() { logic(em -> { A a = new A(); a.setId("A_ID"); a.setName("A"); em.persist(a); B b = new B(); b.setA(a); b.setId("B_ID"); b.setName("B"); em.persist(b); C c = new C(); c.setB(b); c.setId("C_ID"); c.setName("C"); em.persist(c); }); } |
@EmbeddedId 식별 관계
@MapsId 사용이 필요
@Entity @Data public class A { @Id @Column(name = "a_id") private String id; private String name;; } |
@Entity @Data public class B { @MapsId("AId") //private String AId; @ManyToOne @JoinColumn(name = "a_id") private A a; @EmbeddedId private B.BId id; private String name;; @NoArgsConstructor @EqualsAndHashCode @Data @Embeddable public static class BId implements Serializable { private String AId; //@MapsId("AId") @Column(name = "b_id") private String id; } } |
@Entity @Data public class C { @MapsId("BId") //private B.BId BId; @ManyToOne @JoinColumns({ @JoinColumn(name = "a_id"), @JoinColumn(name = "b_id") }) private B b; @EmbeddedId private C.CId id; private String name; @NoArgsConstructor @EqualsAndHashCode @Data @Embeddable public static class CId implements Serializable{ private B.BId BId; //@MapsId("BId") @Column(name = "c_id") private String id; } } |
public static void main(String[] args) { save(); find(); emf.close(); } private static void find() { logic(em -> { A a = em.find(A.class, "A_ID"); BId bId = new B.BId(); bId.setAId("A_ID"); bId.setId("B_ID"); B b = em.find(B.class, bId); CId cId = new C.CId(); cId.setBId(bId); cId.setId("C_ID"); C c = em.find(C.class, cId); }); } private static void save() { logic(em -> { A a = new A(); a.setId("A_ID"); a.setName("A"); em.persist(a); B b = new B(); b.setA(a); BId bId = new B.BId(); bId.setAId("A_ID"); bId.setId("B_ID"); b.setId(bId); b.setName("B"); em.persist(b); C c = new C(); c.setB(b); CId cId = new C.CId(); cId.setBId(bId); cId.setId("C_ID"); c.setId(cId); c.setName("C"); em.persist(c); }); } |
@EmbeddedId 는 @IdClass와 다르게 @Id 대신 @MapsId를 사용한다.
@MapsId 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻
@IdClass, @EmbeddedId 는 엔티티에서 매핑 방식이기에 DB 테이블 생성, 조회 쿼리는 똑같다.
비식별 관계 구현
@Entity @Data public class A { @Id @GeneratedValue @Column(name = "a_id") private Long id; private String name;; } |
@Entity @Data public class B { @Id @GeneratedValue @Column(name = "b_id") private Long id; @ManyToOne @JoinColumn(name = "a_id") private A a; private String name;; } |
@Entity @Data public class C { @Id @GeneratedValue @Column(name = "c_id") private Long id; @ManyToOne @JoinColumn(name = "b_id") private B b; private String name; } |
비식별 관계에서 복합 키를 만들 필요가 없어 식별 관계 사용 시 복합 키를 사용한 것과 비교해 더 쉽다.
일대일 식별 관계
일대일 관계는 자식 테이블이 부모 테이블 기본 키를 외래 키 + 기본 키로 사용한다.
따라서 부모 테이블 기본 키가 복합 키가 아니면, 자식 테이블도 복합 키로 구성하지 않아도 된다.
@Entity @Data public class Board { @Id @GeneratedValue @Column(name = "board_id") private Long id; private String title; @OneToOne(mappedBy = "board") private BoardDetail boardDetail; } |
@Entity @Data public class BoardDetail { @Id private Long id; @MapsId @OneToOne @JoinColumn(name = "board_id") private Board board; private String content; } |
식별 컬럼이 하나면 값을 생략해도 된다.
식별, 비식별 관계의 장단점
DB 관점에서 비식별 관계를 선호하는 이유
부모 테이블의 기본 키를 자식 테이블로 전파한다.
이 특성 때문에 부모-자식 관계가 반복될 경우 가장 끝 자식 테이블의 기본 키는 상위 모든 부모 테이블의 기본 키를 가지게 된다.
기본 키가 많은 수록 인덱스 부하, 조인 시 더 복잡하다.
기본 키 전파 특성 때문에 복합 키를 기본 키로 많들어야 하는 경우가 많다
식별 관계에서 기본 키로 자연 키를 사용할 경우가 많다. 이 경우 시간이 지나 비즈니스 요구사항이 바뀌면, 자식 모두에게 영향을 미쳐 변경이 힘들어 진다.
즉, 테이블 구조가 유연하지 못하다.
객체 관계 매핑 관점에서 비식별 관계를 선호하는 이유
일대일 관계를 제외하고, 식별 관계는 복합 키를 기본 키로 사용해야 한다.
즉, 기본 키 설정이 더 복잡하다.
키 생성 또한, 비식별 관계는 대리 키 자동 생성을 주로 사용하는 데, 식별 관계는 복합 키가 많아 불가능하다.
식별 관계 장점으로 자식 테이블이 기본 키가 거대해 특수한 상황에서 조인없이 기본 키 인덱스로만 조회할 수 있다.
가급적 필수적 비식별 관계를 사용하고 기본 키는 Long형 대리 키를 사용하자
필수적 비식별 관계여야 외부 조인을 사용하지 않는다.
조인 테이블
DB 테이블 연관관계 설계 방법 두 가지
- 조인 컬럼 (외래 키 사용)
- 조인 테이블 (테이블 사용)
연관관계를 별도 테이블로 관리
조인 시 연관관계 테이블까지 조인해야 한다.
다대다 관계를 일대다 - 다대일 관계로 풀기 위한 용도로 주로 사용
일대일 조인 테이블
외래 키 컬럼 모두에 유니크 제약조건이 필요하다
@Entity @Data public class Parent { @Id @GeneratedValue @Column(name = "parent_id") private Long id; private String name; @OneToOne @JoinTable(name = "parent_child", joinColumns = @JoinColumn(name = "parent_id"), inverseJoinColumns = @JoinColumn(name = "child_id", unique = true) ) private Child child; } |
@Entity @Data public class Child { @Id @GeneratedValue @Column(name = "child_id") private Long id; private String name; @OneToOne(mappedBy = "child") private Parent parent; } |
일대다 조인 테이블
다 쪽에 속한 CHILD_ID 에 대한 유니크 제약 조건이 필요하다.
@Entity @Data public class Parent { @Id @GeneratedValue @Column(name = "parent_id") private Long id; private String name; @OneToMany @JoinTable(name = "parent_child", joinColumns = @JoinColumn(name = "parent_id"), inverseJoinColumns = @JoinColumn(name = "child_id") ) private List<Child> childs = new ArrayList<>(); } |
@Entity @Data public class Child { @Id @GeneratedValue @Column(name = "child_id") private Long id; private String name; } |
다대일 조인 테이블
@Entity @Data public class Parent { @Id @GeneratedValue @Column(name = "parent_id") private Long id; private String name; @OneToMany(mappedBy = "parent") private List<Child> childs = new ArrayList<>(); } |
@Entity @Data public class Child { @Id @GeneratedValue @Column(name = "child_id") private Long id; private String name; @ManyToOne(optional = false) @JoinTable(name = "parent_child", joinColumns = @JoinColumn(name = "child_id"), inverseJoinColumns = @JoinColumn(name = "parent_id") ) private Parent parent; } |
대다대 조인 테이블
@Entity @Data public class Parent { @Id @GeneratedValue @Column(name = "parent_id") private Long id; private String name; @ManyToMany @JoinTable(name = "parent_child", uniqueConstraints = {@UniqueConstraint(columnNames = {"parent_id", "child_id"})}, joinColumns = @JoinColumn(name = "parent_id"), inverseJoinColumns = @JoinColumn(name = "child_id") ) private List<Child> childs = new ArrayList<>(); } |
@Entity @Data public class Child { @Id @GeneratedValue @Column(name = "child_id") private Long id; private String name; @ManyToMany(mappedBy = "childs") private List<Parent> parent = new ArrayList<>();; } |
모든 @JoinTable은 외래 키를 제외한 추가 컬럼이 필요 시 사용할 수 없다.
엔티티 하나에 여러 테이블 매핑
@SecondaryTable
한 엔티티에 여러 테이블을 매핑할 수 있다.
@Data @Entity @Table(name = "Board") @SecondaryTable(name = "Board_Detail", pkJoinColumns = @PrimaryKeyJoinColumn(name = "board_detail_id")) public class Board { @Id @GeneratedValue @Column(name = "board_id") private Long id; private String title; @Column(table = "Board_Detail") private String content; } |
@SecondaryTable 은 사용하지 않는 것이 좋다.
항상 두 테이블에서 조회하게 되며 최적화하기 힘들다.
각각 테이블에 매핑되었다면, 한 쪽만 조회 필요 시 조인으로 조회할 수 있다.
실전 예제
상속 관계 매핑 추가
- @Inheritance, @DiscriminatorColumn, @DiscriminatorValue
- @MappedSuperclass
@MappedSuperclass @Data public class BaseEntity { private LocalDateTime createdDate; private LocalDateTime lastModifiedDate; } |
@Entity @Data public class Member extends BaseEntity{ @Id @GeneratedValue @Column(name = "member_id") private Long id; private String name; private String city; private String strret; private String zipcode; @OneToMany(mappedBy = "member") @ToString.Exclude private List<Order> orders = new ArrayList<>(); } |
@Entity @Data public class Delivery extends BaseEntity{ @Id @GeneratedValue @Column(name = "delivery_id") private Long id; @OneToOne(mappedBy = "delivery") private Order order; private String city; private String street; private String zipcode; @Enumerated(EnumType.STRING) private DeliveryStatus status; } |
@Entity @Table(name = "orders") @Data public class Order extends BaseEntity{ @Id @GeneratedValue @Column(name = "order_id") private Long id; @ManyToOne @JoinColumn(name = "member_id") private Member member; @OneToMany(mappedBy = "order") private List<OrderItem> orderItems = new ArrayList<>(); @OneToOne @JoinColumn(name = "delivery_id") private Delivery delivery; private LocalDateTime orderDate; @Enumerated(EnumType.STRING) private OrderStatus status; // 연관관계 편의 메소드 public void setMember(Member member) { if (this.member != null) { this.member.getOrders().remove(this); } this.member = member; member.getOrders().add(this); } public void addOrderItem(OrderItem orderItem) { orderItems.add(orderItem); orderItem.setOrder(this); } public void setDelivery(Delivery delivery) { this.delivery = delivery; delivery.setOrder(this); } } |
@Entity @Table(name = "order_item") @Data public class OrderItem extends BaseEntity { @Id @GeneratedValue @Column(name = "order_item_id") private Long id; @ManyToOne @JoinColumn(name = "item_id") private Item item; @ManyToOne @JoinColumn(name = "order_id") private Order order; private int orderPrice; private int count; public void setOrder(Order order) { if (this.order != null) { this.order.getOrderItems().remove(this); } this.order = order; order.getOrderItems().add(this); } } |
@Entity @Data public class Category extends BaseEntity{ @Id @GeneratedValue @Column(name = "category_id") private Long id; private String name; @ManyToMany @JoinTable(name = "category_item", joinColumns = @JoinColumn(name = "category_id"), inverseJoinColumns = @JoinColumn(name = "item_id")) private List<Item> items = new ArrayList<>(); // 카테고리 계층 구조를 위한 필드들 @ManyToOne @JoinColumn(name = "parent") private Category parent; @OneToMany(mappedBy = "parent") private List<Category> child = new ArrayList<>(); // 연관관계 메소드 public void addChildCategory(Category child) { this.child.add(child); child.setParent(this); } public void addItem(Item item) { items.add(item); } } |
@Data @Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "DTYPE") public abstract class Item extends BaseEntity{ @Id @GeneratedValue @Column(name = "item_id") private Long id; private String name; private int price; private int stockQuantity; @ManyToMany(mappedBy = "items") private List<Category> categories = new ArrayList<>(); } |
@Entity @DiscriminatorValue("A") @Data public class Album extends Item{ private String artist; private String etc; } |
@Entity @DiscriminatorValue("M") @Data public class Movie extends Item{ private String director; private String actor; } |
@Entity @DiscriminatorValue("B") @Data public class Book extends Item{ private String author; private String isbn; } |
'개발 > JPA' 카테고리의 다른 글
09 값 타입 (0) | 2024.05.20 |
---|---|
08 프록시와 연관관계 관리 (1) | 2024.05.13 |
06 다양한 연관관계 매핑 (0) | 2024.04.14 |
05 연관관계 매핑 기초 (1) | 2024.03.22 |
04 엔티티 매핑 (1) | 2024.02.26 |