상속 관계 매핑
조인전략
: 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본키+외래키로 사용하는 전략
- 객체는 타입으로 구분이 가능하지만, 테이블은 타입의 개념이 없기때문에 구분하는 컬럼을 추가해야한다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED) // 상속 매핑
@DiscriminatorColumn(name = "DTYPE") // 구분 컬럼을 지정
public abstract class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
}
@Entity
@DiscriminatorValue("Album") // 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
public class Album extends Item {
private String artist;
}
@Entity
@DiscriminatorValue("Movie") // 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
public class Movie extends Item {
private String director;
private String actor;
}
@Entity
@DiscriminatorValue("Book")
@PrimaryKeyJoinColumn(name = "BOOK_ID") // ITEM_ID 기본키 컬럼명을 BOOK_ID로 변경
public class Book extends Item {
private String author;
private String isbn;
}
@Inheritance(strategy = InheritanceType.JOINED)
- 장점
- 테이블이 정규화
- 외래키 참조 무결정 제약조건을 활용할 수 있다
- 저장공간을 효율적으로 사용한다
- 단점
- 조회할 때 조인이 많이 사용되므로 성능저하가 초래될 수 있음
- 조회 쿼리가 복잡함
- 데이터를 등록할 INSERT의 SQL이 두번실행
단일 테이블 전략
: 테이블을 하나만 사용하는 전략. 조인을 사용하지 않기 때문에 빠르다.
- 테이블 하나에 모든 것을 통합하기때문에 구분 컬럼(@DiscriminatorValue)를 필수로 사용해야한다.
(지정되지 않으면 기본 Class명을 이름으로 사용한다)
- 자식 Entity가 매핑한 컬럼은 모두 null이 허용되어야 한다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 상속 매핑
@DiscriminatorColumn(name = "DTYPE") // 구분 컬럼을 지정
public abstract class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
}
@Entity
@DiscriminatorValue("Album") // 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
public class Album extends Item {
private String artist;
}
@Entity
@DiscriminatorValue("Movie") // 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
public class Movie extends Item {
private String director;
private String actor;
}
@Entity
@DiscriminatorValue("Book") // 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
public class Book extends Item {
private String author;
private String isbn;
}
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
- 장점
- 조인이 필요 없으므로 조회 쿼리가 단순하여 성능이 빠르다
- 단점
- 자신 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다
- 단일 테이블에 모든 것을 저장하므로 테이블이 커지기때문에, 상황에따라서는 성능이 좋지 못할 수도 있다
구현 클래스 테이블 전략
: 자식 엔티티마다 테이블을 만들어서 사용하는 전략
- 구분컬럼(@DiscriminatorValue)를 사용하지 않는다.
- 별로 추천하지 않는 전략이다.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
}
@Entity
public class Album extends Item {
private String artist;
}
@Entity
public class Movie extends Item {
private String director;
private String actor;
}
@Entity
public class Book extends Item {
private String author;
private String isbn;
}
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
- 장점
- subType을 구분해서 처리할 때 좋다
- not null 제약조건을 사용할 수 있다
- 단점
- 여러 자식 테이블을 함께 조회할 때 성능이 느리다 (UNION..)
- 자식테이블을 통합해서 쿼리하기가 까다롭다.
@MappedSuperclass
: 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고 싶을때 사용한다. (실질적인 Entity가 아님)
@MappedSuperclass // 추상클래스와 비슷
public abstract class CommonEntity {
@Id
@GeneratedValue
private Long id;
private String name;
}
@Entity
@AttributeOverride(name = "id", column = @Column(name = "USER_ID")) // 상속받은 컬럼을 재정의
public class User extends CommonEntity {
// id상속
// name상속
private String phone;
private String email;
}
@Entity
@AttributeOverrides({
@AttributeOverride(name = "id", column = @Column(name = "PRODUCT_ID")),
@AttributeOverride(name = "name", column = @Column(name = "PRODUCT_NAME"))
}) // 둘이상 재정의
public class Product extends CommonEntity {
// id상속
// name상속
private Integer price;
private LocalDateTime createDate;
}
공통 속성을 상속받아서 사용하였다. 그리고 상속받은 필드를 재정의하려면 @AttributeOverride를 통해 재정의할 수 있다. (둘이상 재정의하는 경우는 @AttributeOverrides 안에 @AttributeOverride들을 추가)
- 테이블과는 관계가 없고 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모아주는 역할만 한다.
- @MappedSuperclass로 지정한 클래스는 엔티티가 아니기때문에 find()와 같은 엔티티매니저관련 메소드를 사용할 수 없다.
- 일반 클래스로는 생성해서 사용하는 일이 거의 없기때문에 추상 클래스(abstract)로 만들어서 쓰는 것을 권장
복합 키와 식별 관계매핑
- 식별관계: 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본키+외래키로 사용하는 관계
- 비식별관계: 부모 테이블의 기본키를 받아서 자식 테이블의 외래 키로만 사용하는 관계
- 필수적 비식별관계(Mandatory): 외래키에 NULL을 허용하지 않는다. 연관관계를 필수로 맺어야함
- 선택적 비식별관계(Optional): 외래키에 NULL을 허용한다. 연관관계를 맺을지 말지를 선택할 수 있음
복합 키: 비식별 관계 매핑
@IdClass
이미 [JPA - 연관관계]편에서 한번 소개한 바가 있다. 다대다 관계를 복합키를 가지는 중간테이블 형태로 사용할 수 있도록 한다. 이 어노테이션은 객체를 복합키형태로 매핑하는 경우에 사용한다.
@Entity
@IdClass(ParentId.class)
public class Parent {
@Id
@Column(name = "PARENT_ID1")
private String id1;
@Id
@Column(name = "PARENT_ID2")
private String id2;
private String name;
}
public class ParentId implements Serializable {
private String id1;
private String id2;
public ParentId() {
}
public ParentId(String id1, String id2) {
this.id1 = id1;
this.id2 = id2;
}
@Override
public boolean equals(Object o) {
return super.equals(o);
}
@Override
public int hashCode() {
return super.hashCode();
}
}
@Entity
public class Child {
@Id
private String id;
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID1", referencedColumnName = "PARENT_ID1"),
@JoinColumn(name = "PARENT_ID2", referencedColumnName = "PARENT_ID2")
})
// name속성과 동일하면 referencedColumnName속성은 생략가능
private Parent parent;
}
- 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다 (id1, id2)
- Serializable 인터페이스를 구현해야 한다
- equals, hashCode를 구현해야 한다
- 기본 생성자가 있어야 한다
- 식별자 클래스는 public이어야 한다
@EmbeddedId
: 복합키 관계를 조금 더 객체지향적으로 할 수 있다. @IdClass와는 다르게 식별자 클래스에 기본키를 직접 매핑한다.
@Entity
public class Parent {
@EmbeddedId
private ParentId id;
private String name;
}
@Embeddable
public class ParentId implements Serializable {
@Column(name = "PARENT_ID1")
private String id1;
@Column(name = "PARENT_ID2")
private String id2;
public ParentId() {
}
public ParentId(String id1, String id2) {
this.id1 = id1;
this.id2 = id2;
}
@Override
public boolean equals(Object o) {
return super.equals(o);
}
@Override
public int hashCode() {
return super.hashCode();
}
}
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("test_parent");
EntityManager em = emf.createEntityManager();
Parent parent = new Parent();
parentId parentId = new ParendId("myId1", "myId2");
parent.setId(parentId);
parent.setName("parent이름");
em.persist(parent);
ParentId getParentId = new ParentId("myId1", "myId2");
Parent getParent = em.find(Parent.class, getParentId); // 복합키로 데이터 조회
}
@IdClass와 마찬가지로... @Embeddable도
- Serializable 인터페이스를 구현해야 한다
- equals, hashCode를 구현해야 한다
- 기본 생성자가 있어야 한다
- 식별자 클래스는 public이어야 한다
복합 키: 식별 관계 매핑
=> @IdClass와 식별 관계
@Entity
public class Parent { // 부모
@Id
@Column(name = "PARENT_ID")
private String id;
private String name;
}
@Entity
@IdClass(ChildId.class)
public class Child { // 자식
@Id
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent; // 식별 관계는 기본 키와 외래 키를 같이 매핑해야 한다.
@Id
@Column(name = "CHILD_ID")
private String childId;
private String name;
}
@Entity
@IdClass(GrandChildId.class)
public class GrandChild { // 손자
@Id
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID"),
@JoinColumn(name = "CHILD_ID")
})
private Child child;
@Id
@Column(name = "GRANDCHILD_ID")
private String id;
private String name;
}
// 자식 ID, 손자 ID
public class ChildId implements Serializable {
private String parent; // Child.parent 매핑
private String childId; // Child.childId 매핑
// equals, hashCode ..
}
public class GrandChildId implements Serializable {
private ChildId child; // GrandChild.child 매핑
private String id; // GrandChild.id 매핑
// equals, hashCode ..
}
=> @EmbeddedId와 식별 관계
@Entity
public class Parent { // 부모
@Id
@Column(name = "PARENT_ID")
private String id;
private String name;
}
@Entity
public class Child { // 자식
@MapsId("parentId") // @Id대신 사용. 외래키와 매핑한 연관관계를 기본키에도 매핑하겠다는 뜻. / ChildId.parendId 매핑
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;
@EmbeddedId
private ChildId childId;
private String name;
}
@Entity
public class GrandChild { // 손자
@MapsId("childId") // GrandChildId.childId 매핑
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID"),
@JoinColumn(name = "CHILD_ID")
})
private Child child;
@EmbeddedId
private GrandChildId id;
private String name;
}
// 자식 ID, 손자 ID
@Embeddable
public class ChildId implements Serializable {
private String parentId; // @MapsId("parentId")로 매핑
@Column(name = "CHILD_ID")
private String id;
// equals, hashCode ..
}
@Embeddable
public class GrandChildId implements Serializable {
private ChildId childId; // @MapsId("childId")로 매핑
@Column(name = "GRANDCHILD_ID")
private String id;
// equals, hashCode ..
}
위의 식별관계를 비식별관계로 바꾸어보자.
@Entity
public class Parent { // 부모
@Id
@GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
}
@Entity
public class Child { // 자식
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Parent parent;
}
@Entity
public class GrandChild { // 손자
@Id
@GeneratedValue
@Column(name = "GRANDCHILD_ID")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "CHILD_ID")
private Child child;
}
- 복합키가 없기때문에 ~Id 클래스를 생성하지 않았다.
일대일 식별 관계
자식 테이블의 기본 키값으로 부모 테이블의 기본 키 값만 사용한다. 따라서 부모가 복합키가 아니면 자식도 복합키가 아니다.
@Entity
public class Board {
@Id
@GeneratedValue
@Column(name = "BOARD_ID")
private Long id;
private String title;
@OneToOne(mappedBy = "board")
private BoardDetail boardDetail;
}
@Entity
public class BoardDetail {
@Id
private Long boardId;
@MapsId // BoardDetail.boardId 매핑 / 식별자가 한개의 컬럼이니까.. 속성값은 비워두자.
@OneToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
private String content;
}
식별, 비식별 관계의 장단점
- 데이터베이스 설계 관점에서 보면 비식별 관계를 선호한다. (결국 실제 서비스는 데이터베이스 설계 관점에서 시작하는 경우가 대부분이다)
- 식별 관계는 부모 테이블에서 자식 테이블로 전파하면서 자식 테이블의 기본키 컬럼이 늘어나는데, 결국 JOIN할때 SQL이 복잡해지고 기본키 인덱스가 불필요하게 커질 수 있다.
- 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본키를 만들어야 하는 경우가 많다. (+ 별도의 복합 키 클래스를 만들어야함)
- 식별 관계를 사용할때 의미가 있는 자연키 컬럼을 조합하는 경우가 많고, 비식별 관계는 대리 키를 주로 사용한다. 비즈니스 요구사항 변경이 있는 경우 기본키 변경이 어렵기때문에 대리 키를 사용하는 비식별 관계가 운영보수에 유리하다.
- 식별 관계는 자식 테이블은 부모 테이블의 기본키를 따라가는 유연하지 못한 구조를 가진다.
- 식별 관계의 장점으로 자식 테이블에 부모키 정보가 포함되어있으므로 JOIN이 없이도 조회가 가능할 수 있다. (물론 부모의 이름검색등..은 JOIN이 필요해서 한정적이긴 함)
조인테이블
: 데이터베이스 테이블의 연관관계를 설계하는 방법은 크게는 아래의 2가지이다.
- 조인 컬럼 사용 (외래키)
- null을 허용해줘야한다
- 조인 테이블 사용 (테이블 사용)
- 객체와 테이블을 매핑할 때 조인 컬럼은 @JoinColumn으로 매핑하고 조인 테이블은 @JoinTable로 매핑한다.
- 조인 테이블은 주로 다대다 관계를 일대다, 다대일 관계로 풀어내기 위해 사용
일대일 조인 테이블
@Entity
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") // 반대방향 엔티티를 참조하는 외래키
)
private Child child;
}
@Entity
public class Child { // 자식
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
// 양방향 매핑하는 경우
// @OneToOne(mappedBy="child")
// private Parent parent;
}
일대다 조인 테이블
@Entity
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> child = new ArrayList<Child>();
}
@Entity
public class Child { // 자식
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
...
}
다대일 조인 테이블
@Entity
public class Parent { // 부모
@Id
@GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "parent")
private List<Child> child = new ArrayList<Child>();
}
@Entity
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
public class Parent { // 부모
@Id
@GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@ManyToMany
@JoinTable(
name = "PARENT_CHILD",
joinColumns = @JoinColumn(name = "PARENT_ID"),
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
)
private List<Child> child = new ArrayList<Child>();
}
@Entity
public class Child { // 자식
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
...
}
엔티티 하나에 여러 테이블 매핑
잘 사용하지는 않지만 @SecondaryTable 어노테이션을 사용하면 한 엔티티에 여러 테이블을 매핑할 수 있다.
@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;
}
- Board 엔티티는 @Table을 사용해서 BOARD 테이블과 매핑, @SecondaryTable을 사용해서 BOARD_DETAIL 테이블을 추가로 매핑했다.
- @SecondaryTable(name): 매핑할 다른 테이블의 이름을 지정
- @SecondaryTable(pkJoinColumns): 매핑할 다른 테이블의 기본키 컬럼 속성을 지정한다
- 더 많은 테이블을 매핑하려면 @SecondaryTables를 사용한다. (안에 @SecondaryTable로 지정)
참고자료
- 서적 - 자바 ORM 표준 JPA 프로그래밍 제 7장 - 김영한 지음
'SpringFramework > JPA' 카테고리의 다른 글
JPA - 객체지향 쿼리 언어, JPQL (0) | 2021.11.17 |
---|---|
JPA - 값타입 (0) | 2021.11.16 |
JPA - 연관관계 (0) | 2021.11.14 |
JPA - 엔티티 매핑 (0) | 2021.11.12 |
JPA - 영속성 컨텍스트 (0) | 2021.11.03 |