[자바 ORM 표준 JPA 프로그래밍] 7장. 고급 매핑
by Jo
7장. 고급 매핑
7.1 상속 관계 매핑
- 관계형 DB에는 상속이라는 개념 X
- ORM 에서 얘기하는 상속 관계 매핑은 객체의 상속구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것
- 슈퍼타입 서브타입 논리 모델을 실제 물리 모델인 테이블로 구현시 아래와 같은 3가지 방법중 선택 가능
- 각각의 테이블로 변환: 각각을 모두 테이블로 만들고 조회할 때 조인을 사용. => 조인 전략
- 통합 테이블로 변환: 테이블을 하나만 사용해서 통합. => 단일 테이블 전략
- 서브타입 테이블로 변환: 서브타입마다 하나의 테이블을 만듬. => 구현 클래스마다 테이블 전략
7.1.1 조인 전략
- 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 pk 를 pk & fk 로 사용
- ∴ 조회 시 조인을 자주 사용
- !!주의사항!!: 객체는 타입으로 구분할 수 있지만 테이블엔 타입 개념이 없으므로 타입 구분 컬럼 필요함
- 위 예제의 경우 dtype column 이 타입 구분용 컬럼
코드
@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("A")
@PrimaryKeyJoinColumn(name = "item_id")
public class Albumn extends Item {
private String artist;
...
}
...
@Entity
@DiscriminatorValue("M")
@PrimaryKeyJoinColumn(name = "item_id")
public class Movie extends Item {
private String director;
private String actor;
...
}
...
@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "item_id")
public class Book extends Item {
private String author;
private String isbn;
...
}
- @Inheritance(strategy = InheritanceType.JOINED): 상속 매핑시 부모 클래스에 @Inheritance 사용해야 함
- 또한 매핑 전략 지정해줘야 하는데 위 예제에서는 조인 전략을 사용하기 위해 InheritanceType.JOINED 설정
- @DiscriminatorColumn(name = “dtype”): 부모 클래스에 구분 컬럼을 지정
- 기본 값은 DTYPE
- @DiscriminatorValue(“M”): 엔티티 저장시 구분 컬럼에 입력할 값을 지정
- 여기 설정한 값이 구분 컬럼(위 예제에선 dtype) 에 저장됨
- @PrimaryKeyJoinColumn(“item_id”): 자식 테이블의 pk 컬럼명 변경하기 위해 사용
- 기본 값으로는 부모 테이블의 id 컬럼명을 그대로 사용
조인전략 장/단점 및 특징
- 장점:
- 테이블이 정규화 됨
- fk 참조 무결성 제약조건 활용 가능
- 저장공간 효율적으로 사용
- 단점:
- 조회시 조인이 많이 사용되어 성능 저하될 수 있음
- 조회 쿼리가 복잡
- 데이터 등록시 INSERT 쿼리 두번 실행됨
- 특징:
- JPA 표준 명세는 구분 컬럼 사용하도록 하지만 하이버네이트를 비롯한 몇 구현체는 구분 컬럼 없이도 동작함
7.1.2 단일 테이블 전략
- 테이블을 하나만 사용
- 구분 컬럼으로 어떤 자식 데이터가 저장되었는지 구분
- !!주의사항!!: 자식 엔티티가 매핑한 컬럼은 모두 null 사용해야 함
- 다른 자식 엔티티와 매핑된 컬럼은 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("A")
public class Albumn extends Item {
private String artist;
...
}
...
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
private String director;
private String actor;
...
}
...
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
private String author;
private String isbn;
...
}
- @Inheritance(strategy = InheritanceType.SINGLE_TABLE): 단일 테이블 전략은 InhertianceType.SINGLE_TABLE 로 설정
- 구분 컬럼은 필수
단일 테이블 전략 장/단점 및 특징
- 장점:
- 조인 필요없어 일반적으로 조회 성능이 빠름
- 조회 쿼리가 단순
- 단점:
- 자식 엔티티가 매핑한 컬럼은 모두 null 허용해야 함
- 단일 테이블에 다 저장해서 테이블이 커질 수 있음 => 상황에 따라 조회 성능 더 느려질 수도
- 특징:
- 구분 컬럼 필수. 따라서
@DiscriminatorColumn
꼭 설정해야 함 @DiscriminatorValue
지정 안하면 default 값은 엔티티 이름
- 구분 컬럼 필수. 따라서
7.1.3 구현 클래스마다 테이블 전략 (Table-per-Concrete-Class Strategy)
- 자식 엔티티마다 테이블 만듬
- 자식 테이블 각각에 필요한 컬럼 모두 생성
코드
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
...
}
...
@Entity
public class Albumn 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): 구현 클래스마다 테이블 전략은 InheritanceType.TABLE_PER_CLASS 로 설정
구현 클래스마다 테이블 전략 장/단점 및 특징
- 장점:
- 서브 타입 구분해서 처리시 효과적
- not null 제약조건 사용 가능
- 단점:
- 여러 자식 테이블 함께 조회 시 성능 안좋음(UNION 사용해야 하기 때문)
- 자식 테이블 통합해서 쿼리하기 어려움
- 특징
- 구분 컬럼 사용안함
7.2 @MappedSuperclass
- 부모 클래스는 테이블과 매핑하지 않고 상속받는 자식 클래스에게 매핑 정보만 제공하려면
@MappedSuperclass
사용
코드
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue
private Long id;
private String name;
...
}
...
@Entity
public class Member extends BaseEntity {
// id 상속
// name 상속
private String email;
...
}
...
@Entity
public class Seler extends BaseEntity {
// id 상속
// name 상속
private String shopName;
...
}
- BaseEntity 에는 객체들이 주로 사용하는 공통 매핑 정보 정의
- 자식 엔티티에서 상속으로 BaseEntity의 매핑 정보 물려받음
- BaseEntity 는 테이블과 매핑 X
- 상속받은 매핑 정보 재정의 하려면
@AttributeOverride
또는@AttributeOverrides
사용 - 연관관계 재정의 하려면
@AssociationOverride
나@AssociationOverrides
사용
코드
@Entity
@AttributeOverride(name = "id", column = @Column(name = "member_id"))
public class Member extends BaseEntity { ... }
- 위 예시처럼
@AttributeOverride
를 써서 상속받은 id 속성 컬럼명을 재정의 할 수 있음
코드
@Entity
@AttributeOverrides({
@AttributeOverride(name = "id", column = @Column(name = "member_id")),
@AttributeOverride(name = "name", column = @Column(name = "member_name"))
})
public class Member extends BaseEntity { ... }
- 둘 이상을 재정의 하려면
@AttrubuteOverrides
사용
@MappedSuperclass 특징
- 테이블과 매핑되지 않고 매핑 정보 상속 위해 사용
@MappedSuperclass
로 지정한 클래스는 엔티티 아니어서 em.find()나 JPQL 에서 사용 불가- 이 클래스를 직접 생성, 사용할 일 거의 없으므로 추상 클래스로 만드는 것이 권장됨
- 이걸 사용해서 등록일자, 수정일자, 등록자, 수정자 같은 공통 속성 효과적으로 관리 가능
@Entity는 @Entity 또는 @MappedSuperclass 로 지정한 클래스만 상속받을 수 있음
7.3 복합 키와 식별 관계 매핑
7.3.1 식별 관계 vs 비식별 관계
- DB 테이블 사이의 관계는 fk가 pk에 포함되는지 여부에 따라 식별/비식별로 구분
- 최근에는 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계 사용하는 추세
- JPA 에서는 식별, 비식별 관계 둘 다 지원함
식별 관계
- 부모 테이블 pk 내려받아 자식 테이블 pk + fk 로 사용하는 관계
- 예제의 경우 parent 테이블의 pk id 받아서 child 테이블의 pk & fk 로 사용
비식별 관계
- 부모 테이블 pk 내려받아 자식 테이블의 fk 로만 사용하는 관계
- 예제의 경우 parent 테이블의 pk id 받아서 child 테이블의 fk 로만 사용
-
비식별 관계는 (필수적 선택적) 비식별 관계로 나뉨 - 필수적 비식별 관계(Mandatory): 외래키 non-null. 연관관계 필수
- 선택적 비식별 관계(Optional): 외래키 nullable. 연관관계 맺을지 여부 선택 가능
7.3.2 복합키: 비식별 관계 매핑
- ch6에서 나온 것 처럼 JPA에서 복합키 쓰려면
@IdClass
를 쓰거나@EmbeddedId
사용해야 함 - !!아래에 나오는 예제들에서 parent, child 는 객체 상속이랑은 무관함
@IdClass
- 관계형 DB에 가까운 방식
코드
@Entity
@IdClass(ParentId.class)
public class Parent {
@Id
@Column(name = "parent_id1")
pirvate Stirng id1; // ParentId.id1과 연결
@Id
@Colu,n(name = "parent_id2")
private String id2;
private String name; // ParentId.id2와 연결
...
}
...
public class ParentId implements 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) { ... }
@Override
public int hashCode() { ... }
...
}
...
@Entity
public class Child {
@Id
@Column(name = "child_id")
private String id;
@ManyToOne
@JoinColumns({
@JoinColumn(name = "parent_id1",
referencedColumnName = "parent_id1"), // name이랑 referencedColumnName 같으면 생략 가능
@JoinColumn(name = "parent_id2",
referencedColumnName = "parent_id2")
})
private Parent parent;
...
}
...
// 저장
public void save() {
Parent parent = new Parent("myId1", "myId2");
parent.setName("parentName");
// ParentId 따로 만들어주지 않아도 em.persist 호출하면
// 영속성 컨텍스트에 엔티티 등록 직전 내부에서 Parent.id1, Parent.id2 값으로
// 식별자 클래스인 ParentId 생성하여 영속성 컨텍스트 키로 사용
em.persist(parent);
}
// 조회
public void find() {
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);
}
@IdClass
식별자 클래스의 조건- 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 함
- ex) Parent.id1 == ParentId.id1, Parent.id2 == ParentId.id2
- Serializable 인터페이스 implements 하고, equals, hashCode override 해야 함
- 기본 생성자 있어야 함
- 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;
// equals and hashCode 구현
...
}
// 저장
public void save() {
Parent parent = new Parent();
ParentId parentId = new ParentId("myId1", "myId2");
parent.setId(parentId);
parent.setName("parentName");
em.persist(parent);
}
...
// 조회
public void find() {
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);
}
@EmbeddedId
사용시 식별자 클래스 조건@Embeddable
어노테이션 붙여야 함- Serializable 인터페이스 구현 및 equals, hashCode 구현
- 기본 생성자 있어야 함
- public 이어야 함
@EmbeddedId
사용시에는 식별자 클래스를 직접 쓰고@EmbeddedId
어노테이션 붙여주면 됨- 저장 시
@IdClass
랑 달리 식별자 클래스 직접 생성해서 사용함
복합키에는
@GenerateValue
사용 불가. 복합키 구성 요소 컬럼 중 하나에도 X
7.3.3 복합키: 식별 관계 매핑
- 부모 -> 자식 -> 손자 까지 계속 기본키 전달하는 식별관계 예제
- 7.3.2와 마찬가지로
@IdClass
나@EmbeddedId
로 식별자 매핑해야 함
@IdClass 와 식별 관계
코드
@Entity
public class Parent {
@Id
@Column(name = "parent_id")
private String id;
private String name;
...
}
...
@Entity
@IdClass(ChildId.class)
public class Child {
@Id
@Column(name = "child_id")
private String childId;
@Id
@ManyToOne
@JoinColumn(name = "parent_id")
public Parent parent;
private String name;
...
}
...
public class ChildId implements Serializable {
private String childId; // Child.childId 매핑
private String parent; // Child.parent 매핑
// equals and hashCode
...
}
...
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
@Id
@Column(name = "grandchild_id")
private String id;
@Id
@ManyToOne
@JoinColumns({
@JoinColumn(name = "parent_id"),
@JoinColumn(name = "child_id")
})
private Child child;
private String name;
...
}
...
public class GrandChildId implements Serializable {
private String id; // GrandChild.id 매핑
private ChildId child; // GrandChild.child 매핑
// equals and hashCode
...
}
- 식별 관계는 pk와 fk 를 같이 매핑해야 함
- ∴ 식별자 매핑인
@Id
와 연관관계 매핑인@ManyToOne
을 같이 사용하여 동시에 매핑
- ∴ 식별자 매핑인
@EmbeddedId 와 식별 관계
@EmbeddedId
로 식별 관계 구성시에는@MapsId
사용해야 함
코드
@Entity
public class Parent {
@Id
@Column(name = "parent_id")
private String id;
private String name;
...
}
...
@Entity
public class Child {
@EmbeddedId
private ChildId id;
@MapsID("parentId")
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
private String name;
...
}
...
@Embeddable
public class ChildId implements Serializable {
@Column(name = "child_id")
private String id;
private String parentId; // @MapsId("parentId") 로 매핑
// equals and hashCode
...
}
...
@Entity
public class GrandChild {
@EmbeddedId
private GrandChildId id;
@MapsId("childId") // GrandChildId.childId 매핑
@ManyToOne
@JoinColumns({
@JoinColumn(name = "parent_id"),
@JoinColumn(name = "child_id")
})
private Child child;
private String name;
...
}
...
@Embeddable
public class GrandChildId implements Serializable {
@Column("grandchild_id")
private String id;
private ChildId childId; // @MapsId("childId") 로 매핑
// equals and hashCode
...
}
@IdClass
와 달리@Id
대신@MapsId
를 사용함@MapsId
는 fk 와 매핑한 연관관계를 pk 에도 매핑하겠다는 의미- 속성값으로는
@EmbeddedId
를 사용한 식별자 클래스의 pk 필드 지정하면 됨- 위 예제의 경우 ChildId.parentId 필드
7.3.4 비식별 관계로 구현
코드
@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 Stirng name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
...
}
...
@Entity
public class GrandChild {
@Id
@GenertateValue
@Column(name = "grandchild_id")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "child_id")
private Child child;
...
}
- 식별 관계의 복합 키를 사용한 코드에 비해 훨씬 간단함
7.3.5 일대일 식별관계
- 일대일 식별 관계는 부모 테이블 pk 만 자식 테이블의 pk & fk 로 사용하기 때문에 부모 테이블 기본키가 복합키 아니면 복합키로 구성하지 않아도 됨
코드
@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;
...
}
- BoardDetail처럼 식별자가 단순 컬럼 하나면
@MapsId
사용하고 속성 값 비워두면 됨
7.3.6 식별, 비식별 관계 장단점
- DB 설계 관점에서는 아래 이유들로 비식별 관계를 선호
- pk 를 자식 테이블로 전파하면서 점점 pk 컬럼 늘어남
- 이로 인해 조인 복잡해지고 pk 인덱스 불필요하게 커질 수 있음
- 2개 이상 컬럼 합해 복합 기본키 만들어야하는 경우가 많음
- 기본키로 비즈니스 의미가 있는 자연 키 컬럼 조합하는 경우가 많은데, 이런 건 좋지 않음
- 대리키 쓰는게 좋다
- pk 를 자식 테이블로 전파하면서 점점 pk 컬럼 늘어남
- 객체 지향 관점에서는 아래 이유들로 비식별 관계를 선호
- 일대일 관계 제외하고는 복합 기본키를 사용하여 매핑 복잡해짐
- 비식별 관계를 쓰면
@GenerateValue
로 대리키 편하게 사용 가능
- 식별 관계의 장점
- 기본 키 인덱스 활용도가 좋고 특정 상황에 조인 없이 하위 테이블 만으로 검색 가능
- 결론적으로 그냥 비식별 관계 쓰고 대리키 사용하는 게 좋다
- 또한 선택적 비식별 보단 필수적 비식별 관계가 항상 관계 있음 보장되어서 inner join 만 해도 돼서 좋다고 함
7.4 조인 테이블
- DB 테이블 연관관계 설계 방식은 다음 2가지
- 조인 컬럼 사용(FK)
- 조인 테이블 사용(테이블 사용)
조인 컬럼 사용
@JoinColumn
으로 매핑- 연관관계를 조인 컬럼(fk 컬럼) 으로 관리
- fk에 null 허용하는 관계 == 선택적 비식별 관계
- 이 경우 outer join 써야 함
- 연관관계 많지 않으면 null 로 저장되는 경우가 대부분이라는 단점 있음
조인 테이블(=연결 테이블, link table) 사용
@JoinTable
로 매핑- 조인 테이블이라는 별도 테이블을 두고 두 테이블의 fk 로 연관관계 관리
- 테이블을 하나 추가해야 해서 관리해야 할 것도, 조인할 것도 늘어난 다는 단점 있음
- 주로 N:N 을 1:N, N:1 로 풀어내기 위해 사용
7.4.1 1:1 조인 테이블
코드
@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;
private Parent parent;
...
}
- 1:1 조인테이블 단방향 매핑 예제
-
양방향으로 매핑하려면 Child.parent 위에
@OneToOne(mappedBy="child")
추가 @JoinTable
속성
속성 | 설명 |
---|---|
name | 매핑할 조인 테이블 이름 |
joinColumns | 현재 엔티티를 참조하는 외래 키 |
inverseJoinColumns | 반대방향 엔티티를 참조하는 외래 키 |
7.4.2 1:N 조인 테이블
코드
@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;
...
}
- 1:N 단방향 조인 테이블 매핑 예제
- 1:N 관계 만들려면 조인 테이블 컬럼 중 N 쪽 컬럼(위 예제에선 child_id)에 유니크 제약조건 걸어야 함
- 예제 child_id 는 pk 라 유니크 제약조건 걸려있음
7.4.3 N:1 조인 테이블
코드
@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"))
priavte Parent parent;
...
}
- N:1, 1:N 양방향 조인 테이블 매핑 예제
7.4.4 N:N 조인 테이블
코드
@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;
..
}
- N:N 조인 테이블 매핑 예제
- N:N 관계 만들려면 조인 테이블의 두 컬럼 합쳐서 하나의 복합 유니크 제약 걸어야 함
- 위 예제의 (parent_id, child_id 는 복합 기본키라 유니크 제약조건 걸려있음)
조인 테이블에 컬럼 추가시
@JoinTable
전략 사용 불가
엔티티 새로 만들어서 조인 테이블이랑 매핑해야 함
7.5 엔티티 하나에 여러 테이블 매핑
@SecondaryTable
,@SecondaryTables
를 사용하여 한 엔티티에 여러 테이블 매핑 가능- 이거로 두 테이블을 하나의 엔티티에 매핑하는 것보다 테이블당 엔티티 만들어서 매핑하는게 더 효율적
- 하나에 매핑하면 항상 두 테이블 조회해서 최적화 어려움
코드
@Entity
@Table(name = "board") // Board 엔티티를 board 테이블과 매핑
@SecondaryTable(name = "board_detail", // board_detail 테이블과 추가 매핑
pkJoinColumns = @PraimaryKeyJoinColumn(name = "board_detail_id"))
/** 두 개 이상 매핑하려면 @SecondaryTables 사용하면 됨
*
* @SecondaryTables({
* @SecondaryTable(name = "board_detail"),
* @SecondaryTable(name = "board_file")})
*/
public class Board {
@Id
@GeneratedValue
@Column(name = "board_id")
private Long id;
@Column(table = "board_detail") // board_detail 테이블의 content 컬럼에 매핑
private String content;
private String title; // title 처럼 따로 지정 안해주면 기본 테이블(여기선 board)에 매핑
...
}
@SecondaryTable
속성
속성 | 설명 |
---|---|
name | 매핑할 다른 테이블의 이름 |
pkJoinColumns | 매핑할 다른 테이블의 pk 컬럼 |
Subscribe via RSS