상속 관계 매핑
- ORM에서 이야기하는 상속 관계 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것
- 슈퍼타입 서브타입 논리 모델을 실제 물리 모델인 테이블로 구현하는 3가지 방법
1) 각각의 테이블로 변환 (조인 전략) : 각각을 모두 테이블로 만들고 조회할 때 조인을 사용
2) 통합 테이블로 변환 (단일 테이블 전략) : 테이블을 하나만 사용해서 통합
3) 서브타입 테이블로 변환 (구현 클래스마다 테이블 전략) : 서브 타입마다 하나의 테이블을 생성 - 조인 전략
- 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략
- 조회할 때 조인을 자주 사용
- 객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없으므로 타입을 구분하는 컬럼을 추가해야 함
// 조인 전략 매핑
@Entity
// 상속 매핑은 부모 클래스에 @Inheritance를 사용하며 매핑 전략으로 조인 전략 사용
// 부모 클래스에 구분 컬럼을 지정하기 위해 @DiscriminatorColumn을 사용해 저장된 자식 테이블 구분
// Album의 경우 엔티티를 저장할 때 DTYPE에 값 A가 저장되고, Movie는 값 M이 저장
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID") // PK
private Long id;
private String name; // 이름
private int price; // 가격
...
}
@Entity
@DiscriminatorValue("A") // 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
public class Album extends Item {
private String artist;
...
}
@Entity
@DiscriminatorValue("M") // 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
public class Movie extends Item {
private String director; // 감독
private String actor; // 배우
...
}
@Entity
@DiscriminatorValue("B")
// 기본값으로는 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용하는데,
// 만약 자식 테이블의 기본 키 컬럼명을 변경하고 싶으면 @PrimaryKeyJoinColumn 사용
@PrimaryKeyJoinColumn(name = "BOOK_ID") // ID 재정의 (ITEM_ID -> BOOK_ID)
public class Book extends Item {
private String author; // 작가
private String isbn; // ISBN (국제표준도서번호)
...
}
-
- 조인 전략의 장점
1) 테이블이 정규화됨
2) 외래 키 참조 무결성 제약조건을 활용할 수 있음
3) 저장공간을 효율적으로 사용 가능 - 조인 전략의 단점
1) 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있음
2) 조회 쿼리가 복잡함
3) 데이터를 등록할 때 INSERT SQL을 두 번 실행해야 함 - 조인 전략의 특징
1) JPA 표준 명세는 구분 컬럼을 사용하도록 하지만 하이버네이트를 포함한 몇몇 구현체는 구분 컬럼 없이도 동작
2) 관련 어노테이션으로는 @PrimaryKeyJoinColumn, @DiscriminatorColumn, @DiscriminatorValue
- 조인 전략의 장점
- 단일 테이블 전략
- 이름 그대로 테이블을 하나만 사용하며 구분 컬럼으로 어떤 자식 데이터가 저장되었는지 구분
- 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 함
Book 엔티티를 저장하면 ITEM 테이블의 AUTHOR, ISBN 컬럼만 사용하고
다른 엔티티와 매핑된 컬럼은 사용하지 않으므로 null이 입력되므로 모두 null을 허용해야 함
// 단일 테이블 전략 매핑
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 단일 테이블 전략 사용
@DiscriminatorColumn(name = "DTYPE") // 테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수로 사용
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID") // PK
private Long id;
privte String name; // 이름
private int price; // 가격
...
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item { ... }
@Entity
@DiscriminatorValue("M")
public class Movie extends Item { ... }
@Entity
@DiscriminatorValue("B")
public class Book extends Item { ... }
-
- 단일 테이블 전략의 장점
1) 조인이 필요 없으므로 일반적으로 조회 성능이 빠름
2) 조회 쿼리가 단순 - 단일 테이블 전략의 단점
1) 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 함
2) 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있으므로 상황에 따라서는 조회 성능이 느려질 수도 있음 - 단일 테이블 전략의 특징
1) 구분 컬럼을 꼭 사용해야 하므로 @DiscriminatorColumn을 꼭 설정
2) @DiscriminatorValue를 지정하지 않으면 기본으로 엔티티 이름(Movie, Album, Book)을 사용
- 단일 테이블 전략의 장점
- 구현 클래스마다 테이블 전략
- 자식 엔티티마다 테이블을 만들며 자식 테이블 각각에 필요한 컬럼이 모두 존재
// 구현 클래스마다 테이블 전략 매핑
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) // 구현 클래스마다 테이블 전략 사용
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID") // PK
private Long id;
private String name; // 이름
pricate int price; // 가격
...
}
@Entity
public class Album extends Item { ... } // 자식 엔티티마다 테이블 생성
@Entity
public class Movie extends Item { ... }
@Entity
public class Book extends Item { ... }
-
- 구현 클래스마다 테이블 전략의 장점
1) 서브 타입을 구분해서 처리할 때 효과적
2) not null 제약조건을 사용할 수 있음 - 구현 클래스마다 테이블 전략의 단점
1) 여러 자식 테이블을 함께 조회할 때 SQL의 UNION을 사용해야 하므로 성능이 느림
2) 자식 테이블을 통합해서 쿼리하기 어려움 - 구현 클래스마다 테이블 전략의 특징
1) 구분 컬럼을 사용하지 않음
2) 데이터베이스 설계자와 ORM 전문가 둘 다 추천하지 않는 전략
- 구현 클래스마다 테이블 전략의 장점
@MappedSuperclass
- 부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보가 제공하고 싶을 때 사용
즉, 단순히 매핑 정보를 상속할 목적으로만 사용 - 회원과 판매자는 서로 관계가 없는 테이블과 엔티티일 때,
테이블은 그대로 두고 객체 모델의 id, name 두 공통 속성을 부모 클래스로 모으고 객체 상속 관계로 만듦
그 후 자식 엔티티에게 공통으로 사용되는 매핑 정보만 제공
// @MappedSupserclass 매핑
@MappedSuperclass
// 객체들이 주로 사용하는 공통 매핑 정보를 정의
// 테이블과 매핑할 필요가 없고 자식 엔티티에게 공통으로 사용되는 매핑 정보만 제공
public abstract class BaseEntity {
@Id @GeneratedValue
private Long id; // PK
private String name;
...
}
@Entity
// 자식 엔티티들은 상속을 통해 BaseEntity의 매핑 정보를 물려 받음
// 부모로부터 물려받은 매핑 정보를 재정의하려면 @AttributeOverrides나 @AttributeOverride를 사용
// 연관관계를 재정의하려면 @AssociationOverrides나 @AssociationOverride를 사용
// 여기서는 부모에게 상속받은 id 속성의 컬럼명을 MEMBER_ID로 재정의
@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID"))
public class Member extends BaseEntity {
// ID 상속
// NAME 상속
private String email;
...
}
@Entity
// 둘 이상을 재정의하려면 @AttributeOverrides를 사용
@AttributeOverrides({
@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID")),
@AttributeOverride(name = "name", column = @Column(name = "MEMBER_NAME"))
})
public class Seller extends BaseEntity {
// ID 상속
// NAME 상속
private String shopName;
...
}
- @MappedSuperclass의 특징
1) 테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용
2) @MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용할 수 없음
3) 이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장
즉, 등록일자, 수정일자, 등록자, 수정자 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있음
복합 키와 식별 관계 매핑
- 식별 관계 vs 비식별 관계
데이터베이스 테이블 사이에 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별 관계와 비식별 관계로 구분
데이터베이스 테이블을 설계할 때 식별 관계나 비식별 관계 중 하나를 선택해야하며,
최근에는 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 추세
- 식별 관계
부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계 - 비식별 관계
부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계 비식별 관계는 외래 키에 NULL을 허용하냐에 따라 필수적 비식별 관계와 선택적 비식별 관계로 나뉘며,
이에 따라 연관관계가 필수적으로 맺어져야만 할 수도 있으며, 반대로 맺을지 말지 선택할 수 있음
- 식별 관계
- 복합 키 : 비식별 관계 매핑
기본 키를 구성하는 컬럼이 하나면 단순하게 매핑
@Entity
public class Hello {
@Id
private String id; // PK
}
둘 이상의 컬럼으로 구성된 복합 기본 키는 별도의 식별자 클래스를 생성해야 함
JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용하며,
식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 이를 구분하기 위해 equals와 hashCode를 구현해 동등성 비교
JPA는 복합 키를 지원하기 위해 @IdClass와 @EmbeddedId 2가지 방법을 제공하며
@IdClass는 관계형 데이터베이스에 가까운 방법이고 @EmbeddedId는 좀 더 객체지향에 가까운 방법
- @IdClass
예) 복합 기본 키를 사용하며 비식별 관계인 복합 키 테이블 PARENT, 부모 테이블의 복합 키 2개를 외래 키로 연결한 CHILD
// 기본 키를 PARENT_ID1, PARENT_ID2로 묶은 복합 키로 구성하는 PARENT 테이블
// 이러한 복합 키를 매핑하기 위해 식별자 클래스를 별도로 생성해야 함
// 부모 클래스
@Entity
@IdClass(ParendId.class) // @IdClass를 사용해 ParentId 클래스를 식별자 클래스로 지정
public class Parent {
@Id // 기본 키를 @Id로 매핑
@Column(name = "PARENT_ID1") // PK
private Stirng id1; // ParentId.id1과 연결
@Id // 기본 키를 @Id로 매핑
@Column(name = "PARENT_ID2") // PK
private Stirng id2; // ParentId.id2와 연결
private String name;
...
}
// 식별자 클래스
/* @IdClass를 사용하기 위해 식별자 클래스가 만족해야 하는 조건
1. 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 함
예) 예제의 Parent.id1과 ParendID.id1, 그리고 Parent.id2와 ParendID.id2가 같아야 함
2. Serializable 인터페이스를 구현해야 함
3. equals, hashCode를 구현해야 함
4. 기본 생성자가 있어야 함
5. 식별자 클래스는 public이어야 함 */
public class ParentId implements Serializable { // public인 식별자 클래스, Serializable 인터페이스
private String id1; // Parent.id1 매핑
private String id2; // Parent.id2 매핑
public ParentId() { // 기본 생성자
}
public ParentId(String id1, String id2) {
this.id1 = id1;
this.id2 = id2;
}
@Override
public boolean equals(Object o) { ... } // equals
@Override
public int hashCode() { ... } // hashCode
}
// 복합 키를 사용하는 엔티티 저장
Parent parent = new Parent();
parent.setId1("myId1"); // 식별자
parent.setId2("myId2"); // 식별자
parent.setName("parentName");
/* em.persist()를 호출하면 영속성 컨텍스트에 엔티티를 등록하기 직전에
내부에서 Parent.id1, Parent.id2 값을 사용해서 식별자 클래스인 ParentId를 생성하고
영속성 컨텍스트의 키로 사용 */
em.persist(parent);
// 복합 키로 조회
// 식별자 클래스인 ParentId를 사용해서 엔티티를 조회
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);
// 자식 클래스
@Entity
public class Child {
@Id
private String id; // PK
@ManyToOne // FK
// 부모 테이블의 기본 키 컬럼이 복합 키이므로 자식 테이블의 외래 키도 복합 키
@JoinColumns({ // 외래 키 매핑 시 여러 컬럼을 매핑하기 위해 @JoinColumns 사용
@JoinColumn(name = "PARENT_ID1", // 각각의 외래 키 컬럼을 @JoinColumn으로 매핑
referencedColumnName = "PARENT_ID1"), // name 속성과 값이 같으므로 생략 가능
@JoinColumn(name = "PARENT_ID2",
referencedColumnName = "PARENT_ID2")
})
private Parent parent;
}
- @EmbeddedId
좀 더 객체지향적인 방법
// 부모 클래스
// 식별자 클래스를 직접 사용하고 @EmbeddedId 어노테이션을 적어주면 됨
@Entity
public class Parent {
@EmbeddedId
private ParentId id; // PK
private String name;
...
}
// 식별자 클래스
// 식별자 클래스에 기본 키를 직접 매핑
/* @EmbeddedId를 적용한 식별자 클래스가 만족해야 하는 조건
1. @Embeddable 어노테이션을 붙여주어야 함
2. Serializable 인터페이스를 구현해야 함
3. equals, hashCode를 구현해야 함
4. 기본 생성자가 있어야 함
5. 식별자 클래스는 public이어야 함 */
@Embeddable
public class ParentId implements Serializable {
@Column(name = "PARENT_ID1")
private Stirng id1;
@Column(name = "PARENT_ID2")
private String id2;
// equals and hashCode 구현
...
}
// 엔티티를 저장
Parent parent = new Parent();
ParentId parentId = new ParentId("myId1", "myId2");
parent.setId(parentId); // 식별자 클래스 parentId를 직접 생성해서 사용
parent.setName("parentName");
em.persist(parent);
// 엔티티 조회
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId); // 식별자 클래스 parentId를 직접 사용
- 복합 키와 equals(), hashCode()
복합 키는 equals()와 hashCode()를 필수로 구현해야 함
영속성 컨텍스트는 엔티티의 식별자를 키로 사용해서 엔티티를 관리하고 식별자를 비교할 때 equals()와 hashCode() 사용
따라서 식별자 객체의 동등성(equals 비교)이 지켜지지 않으면 예상과 다른 엔티티가 조회되거나
엔티티를 찾을 수 없는 등 영속성 컨텍스트가 엔티티를 관리하는데 심각한 문제가 발생할 수 있으므로
복합 키는 equals()와 hashCode()를 필수로 구현해야 함
예) 동등성 비교
ParentId id1 = new ParentId();
id1.setId1("myId1");
id1.setId2("myId2");
ParentId id2 = new ParentId();
id2.setId1("myId1");
id2.setId2("myId2");
/* id1과 id2 인스턴스 둘 다 myId1, myId2라는 같은 값을 가지고 있지만 인스턴스는 다름
그렇기 때문에 equals()를 적절히 오버라이딩했다면 참이겠지만
적절히 오버라이딩 하지 않았다면 결과는 거짓
자바의 모든 클래스는 기본으로 Object 클래스를 상속받는데
이 클래스가 제공하는 기본 equals()는 인스턴스 참조 값 비교인 == 비교(동일성 비교)를 하므로 */
id1.equals(id2) -> ?
- @IdClass vs @EmbeddedId
@IdClass와 @EmbeddedId는 각각 장단점이 있으므로 취향에 맞춰 일관성 있게 사용
@EmbeddedId가 더 객체지향적이고 중복도 없어서 좋아보이지만 특정 상황에 JPQL이 길어질 수 있음
// 특정 상황의 JPQL
em.createQuery("select p.id.id1, p.id.id2 from Parent p"); // @EmbeddId
em.createQuery("select p.id1, p.id2 from Parent p"); // @IdClass
+) 복합 키에는 @GeneratedValue를 하나에도 사용할 수 없음
- 복합 키 : 식별 관계 매핑
식별 관계에서 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야 하므로
@IdClass나 @EmbeddedId를 사용해 식별자를 매핑
예) 부모, 자식, 손자까지 계속 기본 키를 전달하는 식별 관계
- @IdClass와 식별 관계
// @IdClass로 식별 관계 매핑
// 부모
@Entity
public class Parent {
@Id @Column(name = "PARENT_ID") // PK
private String id;
private String name;
...
}
// 자식
@Entity
@IdClass(ChildId.class)
public class Child {
/* 식별 관계는 기본 키와 외래 키를 같이 매핑해야 하므로
식별자 매핑인 @Id와 연관관계 매핑인 @ManyToOne을 같이 사용 */
@Id // 기본 키 매핑
@ManyToOne // 외래 키 매핑
@JoinColumn(name = "PARENT_ID") // PK + FK
public Parent parent;
@Id @Column(name = "CHILD_ID") // PK
private String childId;
private String name;
...
}
// 자식 ID (자식 식별자 클래스)
public class ChildId implements Serializable {
private String parent; // Child.parent 매핑
private String childId; // Child.childId 매핑
// euqals, hashCode
...
}
// 손자
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
@Id
@ManyToOne
@JoinColumns({ // PK + FK
@JoinColumn(name = "PARENT_ID"),
@JoinColumn(name = "CHILD_ID")
})
private Child child;
@Id @Column(name = "GRANDCHILD_ID") // PK
private String id;
private String name;
...
}
// 손자 ID (손자 식별자 클래스)
public class GrandChildId implements Serializable {
private ChildId child; // GrandChild.child 매핑
private String id; // GrandChild.id 매핑
// euqals, hashCode
...
}
-
- @EmbeddedId와 식별 관계
// @EmbeddedId로 식별 관계 매핑하기
// 부모
@Entity
public class Parent {
@Id @Column(name = "PARENT_ID") // PK
private String id;
private String name;
...
}
// 자식
@Entity
public class Child {
@EmbeddedId
private ChildId id;
/* 식별 관계로 사용한 연관관계의 속성에 @Id 대신 @MapsId 사용하며
외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻 (PK + FK)
@MapsId의 속성 값은 EmbeddedId를 사용한 식별자 클래스의 기본 키 필드를 지정 */
@MapsId("parendId") // ChildId.parentId 매핑 (식별자 클래스의 기본 키 필드인 parentId)
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;
private String name;
...
}
// 자식 ID (자식 식별자 클래스)
@Embeddedable
public class ChildId implements Serializable {
private String parentId; // @MapsId("paredId")로 매핑, PK + FK
@Column(name = "CHILD_ID") // PK
private String id;
// euqals, hashCode
...
}
// 손자
@Entity
public class GrandChild {
@EmbeddedId
private GrandChildId id;
@MapsId("childId") // GrandChilId.childId 매핑
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID"),
@JoinColumn(name = "CHILD_ID")
})
private Child child;
private String name;
...
}
// 손자 ID (손자 식별자 클래스)
@Embeddeable
public class GrandChildId implements Serializable {
private ChildId childId; // @MapsId("childId")로 매핑, PK + FK
@Column(name = "GRANDCHILD_ID") // PK
private String id;
// euqals, hashCode
...
}
- 비식별 관계로 구현
식별 관계 테이블을 비식별 관계로 변경 후, 비식별 관계로 만든 테이블을 매핑
// 비식별 관계 매핑하기
// 식별 관계의 복합 키를 사용한 코드와 비교하면 매핑도 쉽고 코드도 단순
// 복합 키가 없으므로 복합 키 클래스를 만들지 않아도 됨
// 부모
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID") // PK
private Long id;
private String name;
...
}
// 자식
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID") // PK
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "PARENT_ID") // FK
public Parent parent;
...
}
// 손자
@Entity
public class GrandChild {
@Id @GeneratedValue
@Column(name = "GRANDCHILD_ID") // PK
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "CHILD_ID") // FK
private Child child;
...
}
- 일대일 식별 관계
// 일대일 식별 관계 매핑
// 일대일 식별 관계는 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용
// 그러므로 부모 테이블의 기본 키가 복합 키가 아니면 자식 테이블의 기본 키는 복합 키를 구성하지 않아도 됨
// 부모
@Entity
public class Board {
@Id @GeneratedValue
@Column(name = "BOARD_ID") // PK
private Long id;
private String title;
@OneToOne(mappedBy = "board")
private BoardDetail boardDetail;
...
}
// 자식
@Entity
public class BoardDetail {
@Id
private Long boardId; // PK
/* 식별자가 단순히 컬럼 하나면 @MapsId를 사용하고 속성 값은 비워두면 되며
@Id를 사용해서 식별자로 지정한 BoardDetail.boardId와 매핑됨 */
@MapsId // BoardDetail.boardId 매핑
@OneToOne
@JoinColumn(name = "BOARD_ID") // FK
private Board board;
private String content;
...
}
// 일대일 식별 관계 저장
public void save() {
Board board = new Board();
board.setTitle("제목");
em.persist(board);
BoardDetail boardDetail = new BoardDetail();
boardDetail.setContent("내용");
boardDetail.setBoard(board);
em.persist(boardDetail);
}
- 식별, 비식별 관계의 장단점
- 데이터베이스 설계 관점에서 식별 관계보다는 비식별 관계를 선호하는 이유
- 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어나게 되므로
결국 조인할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있음 - 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많음
- 비식별 관계의 기본 키는 비즈니스와 전혀 관계없는 대리 키를 주로 사용하는 반면,
식별 관계를 사용할 때 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많음
하지만 비즈니스 요구사항은 시간이 지남에 따라 언젠가는 변하므로
식별 관계의 자연 키 컬럼들이 자식에 손자까지 전파되면 변경하기 힘듦 - 식별 관계는 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하므로 비식별보다 테이블 구조가 유연하지 못함
- 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어나게 되므로
- 객체 관계 매핑의 관점에서 식별 관계보다 비식별 관계를 선호하는 이유
- 일대일 관계를 제외하고 식별 관계는 2개 이상의 컬럼을 묶은 복합 기본 키를 사용하는데
JPA에서 복합 키는 별도의 복합 키 클래스를 만들어 사용해야 하므로 컬럼이 하나인 기본 키 매핑보다 노력이 필요 - 비식별 관계의 기본 키는 주로 대리 키를 사용하는데 JPA는 @GeneratedValue처럼 편리한 대리 키 생성 방법을 제공
- 일대일 관계를 제외하고 식별 관계는 2개 이상의 컬럼을 묶은 복합 기본 키를 사용하는데
- 식별 관계가 가지는 장점
- 기본 키 인덱스를 활용하기 좋음
- 데이터베이스 설계 관점에서 식별 관계보다는 비식별 관계를 선호하는 이유
// 기본 키 인덱스를 활용하는 예
// CHILD 테이블의 기본 키 인덱스를 PARENT_ID + CHILD_ID로 구성하면
// 별도의 인덱스를 생성할 필요 없이 기본 키 인덱스만 사용해 활용 가능
// 예1) 부모 아이디가 A인 모든 자식 조회
SELECT * FROM CHILD
WHERE PARENT_ID = 'A'
// 예2) 부모 아이디가 A고 자식 아이디가 B인 자식 조회
SELECT * FROM CHILD
WHERE PARENT_ID = 'A' AND CHILD_ID = 'B'
-
-
- 상위 테이블들의 기본 키 컬럼을 자식, 손자 테이블이 가지고 있어 특정 상황에 조인 없이 하위 테이블만으로 검색 가능
- 식별 관계가 가지는 장점과 단점이 있으므로 꼭 필요한 곳에는 적절하게 식별 관계를 사용하는 것이 방법
- ORM 신규 프로젝트 진행시 추천하는 방법은 될 수 있으면 비식별 관계를 사용하고 기본 키는 Long 타입의 대리 키를 사용
대리 키는 비즈니스와 아무 관련이 없으므로 비즈니스가 변경되어도 유연한 대처가 가능하며
JPA는 @GeneratedValue를 통해 간편하게 대리 키를 생성할 수 있고 식별자 컬럼이 하나여서 쉽게 매핑 가능 - 또한 NULL을 허용하여 조인할 때 외부 조인을 사용해야하는 선택적 비식별 관계보다는
NOT NULL로 항상 관계가 있다는 것을 보장하는 필수적 비식별 관계를 사용하여 내부 조인만 사용할 수 있어 이를 추천
-
조인 테이블
- 데이터베이스 테이블의 연관관계를 설계하는 두 가지 방법
- 조인 컬럼 사용 (외래 키) : 매핑을 위해 @JoinColumn 사용
테이블 간에 관계는 주로 조인 컬럼이라고 부르는 외래 키 컬럼을 사용해서 관리
예) 회원과 사물함
회원이 사물함을 사용하기 전까지는 아직 둘 사이에 관계가 없으므로 MEMBER 테이블의 LOCKER_ID 외래 키에 null 입력
이렇게 외래 키에 null을 허용하는 관계를 선택적 비식별 관계라고 하며, 회원과 사물함을 조인 시 외부 조인을 사용해야 함
내부 조인을 사용할 경우 사물함과 관계 없는 회원은 조회되지 않으므로
회원과 사물함이 아주 가끔 관계를 맺는다면 외래 키 값 대부분이 null로 저장되어 내부 조인을 할 수 없는 단점이 존재 - 조인 테이블 사용 (테이블 사용) : 매핑을 위해 @JoinTable 사용
조인 테이블이라는 별도의 테이블을 사용해서 연관관계를 관리
조인 컬럼을 사용하는 방법은 단순히 외래 키 컬럼만 추가해서 연관관계를 맺지만
조인 테이블을 사용하는 방법은 연관관계를 관리하는 조인 테이블을 추가하고 여기서 두 테이블의 외래 키를 가지고
연관관계를 관리하므로 따라서 MEMBER와 LOCKER 테이블에는 연관관계를 관리하기 위한 외래 키 컬럼이 없음
조인 테이블의 경우 테이블을 하나 추가해야 하므로 관리해야 하는 테이블이 늘어나고
회원과 사물함 두 테이블을 조인하려면 MEBMER_LOCKER 테이블까지 추가로 조인해야한다는 단점이 존재
예) 회원과 사물함
회원과 사물함 데이터를 각각 등록했다가 회원이 우너할 때 사물함을 선택하면 MEMBER_LOCKER 테이블에만 값을 추가 - 기본은 조인 컬럼을 사용하고 필요하다고 판단되면 조인 테이블을 사용하자.
- 조인 컬럼 사용 (외래 키) : 매핑을 위해 @JoinColumn 사용
- 일대일 조인 테이블
일대일 관계를 만들려면 조인 테이블의 외래 키 컬럼(PARENT_ID, CHILD_ID) 각각에 총 2개의 유니크 제약 조건을 걸어야 함
// 일대일 조인 테이블 매핑
// 부모
@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; */
...
}
- 일대다 조인 테이블
일대다 관계를 만들려면 조인 테이블의 컬럼 중 다(N)와 관련된 컬럼(CHILD_ID)에 유니크 제약 조건을 걸어야 함
// 일대다 단방향 조인 테이블 매핑
// 부모
@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;
...
}
- 다대다 조인 테이블
다대다 관계를 만들려면 조인 테이블의 두 컬럼(PARENT_ID, CHILD_ID)을 합해서 하나의 복합 유니크 제약 조건을 걸어야 함
// 다대다 조인 테이블 매핑
// 부모
@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;
...
}
- 만약 조인 테이블에 컬럼을 추가하면 @JoinTable을 쓸 수 없으므로 이 때는 새로운 엔티티를 만들어서 조인 테이블과 매핑
엔티티 하나에 여러 테이블 매핑
- @SecondaryTable을 사용해서 한 엔티티에 여러 테이블을 매핑하며 더 많은 테이블을 매핑하려면 @SecondaryTables 사용
참고로 @SecondaryTable을 사용해서 두 테이블을 하나의 엔티티에 매핑하는 방법보다는
테이블당 엔티티를 각각 만들어서 일대일 매핑하는 것을 권장하며
두 테이블을 하나의 엔티티에 매핑할 경우 항상 두 테이블을 조회하므로 최적화하기 어려우므로
일대일 매핑을 통해 원하는 부분만 조회하고 필요하면 둘을 함께 조회
// 하나의 엔티티에 여러 테이블 매핑
@Entity
// @Table을 사용해 Board 엔티티를 BOARD 테이블과 매핑
@Table(name = "BOARD")
// @SecondaryTable을 사용해 BOARD_DETAIL 테이블을 추가로 매핑
@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; // 테이블을 지정하지 않으면 기본 테이블인 BOARD에 매핑
@Column(table = "BOARD_DETAIL")
private String content; // @Colum을 사용해서 BOARD_DETAIL 테이블의 컬럼에 매핑
...
}
/* @SecondaryTables 사용 예제
@SecondaryTables({
@SecondaryTable(name = "BOARD_DETAIL"),
@SecondaryTable(name = "BOARD_FILE")
}) */
'Java-Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 프록시와 연관관계 관리 (0) | 2022.04.14 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 고급 매핑 - 실전 예제 (0) | 2022.04.12 |
[자바 ORM 표준 JPA 프로그래밍] 다양한 연관관계 매핑 - 실전 예제 (0) | 2022.04.04 |
[자바 ORM 표준 JPA 프로그래밍] 다양한 연관관계 매핑 (0) | 2022.04.04 |
[자바 ORM 표준 JPA 프로그래밍] 연관관계 매핑 기초 - 실전 예제 (0) | 2022.03.31 |