JPA part 7
고급 매핑
상속관계 매핑
객체 지향형 프로그래밍에서는 아래와 같은 객체 간 상속을 지원한다.
RDBMS에서 상속 모델을 구현하기 위해 3가지 방식을 지원하는데, 1:1, 싱글, 슈퍼 + 서브타입 방식이 존재한다.
각가의 방식으로 상속관계를 생성할 수 있고, 부모 클래슨 abstract, 자식 클래스는 이를 구현하는 방식으로 자바에서는 설계하면 된다.
1:1
흔히, 조인전략이라고 말하는 이 방식은 부모 클래스, 자식 클래스에 대해 모두 테이블로 만들어서 기본키 + 외래키를 이용한 조인방식으로 상속관계를 표현한다.
ITEM CLASS (부모 클래스)
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DicriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id
@GeneratedValue
@Column (name = "ITEM_ID")
private Long id;
private String name;
private int price;
}
Child Classes
@Entity
@DicriminatorValue("A")
public class Album extends Item {
private String artist;
...
}
@Entity
@DicriminatorValue("M")
public class Movie extends Item {
private String director;
private String actor;
...
}
@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID")
public class Book extends Item {
private String author;
private String isbn;
}
Annotations | Description |
---|---|
@Inheritance(strategy = InheritanceType.JOINED) | 상속 매핑을 하고자 할때 해당 annotation을 활용하며, 방식을 지정해준다. 여기서는 조인전략 방식의 상속매핑이다. |
@DicriminatorColumn(name = “DTYPE”) | 자식 테이블을 구분하기 위한 컬럼을 추가한다. |
@DicriminatorValue(“A”) | 구분 컬럼의 값을 지정한다.(Discriminator Column) |
@PrimaryKeyJoinColumn(name = “BOOK_ID”) | 자식 테이블은 기본값으로 부모 ID 컬럼명을 그대로 사용하는데, 이 annotation을 통해 ID 컬럼명 변경 가능 |
장점
- 테이블이 정규화됨
- 외래키 참조 무결성 제약조건 활용(조인을 통한 조회)
- 저장공간이 효율적이다
단점
- 조인 성능이 안 좋다(매번 많은 조인이 필요하기 때문)
- 조회가 복잡하다(모든 테이블이 다 분리되어 있기 때문)
- INSERT 문을 부모 클래스, 자식 클래스 두번에 걸쳐 진행된다.
Single Table
부모 클래스와 자식 테이블을 하나의 테이블로 구성하고, DTYPE 컬럼을 이용해서 자식 클래스를 구분한다.
Single Class (부모 클래스 + 자식 클래스)
@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 Album extends Item {
...
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
...
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
...
}
여기서는 Inheritance type를 SIGLE_TABLE로 지정한다.
장점
- 조회 성능이 가장 좋다(테이블 개수가 1개 이므로)
- 조회 쿼리가 간단해진다.
단점
- 자식 테이블과 관련된 모든 컬럼이 한 테이블에 포함되므로 NULL 값인 컬럼이 많이 존재한다.
- 오히려, 컬럼이 많아 지면서 조회 성능이 떨어질 수도 있다.
SuperType + SingleType
자식 테이블을 각각 만들어 주며, 각각의 자식 테이블에 부모 테이블 관련 컬럼을 추가해주는 방식이다.
Super + Sub Classes
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
@Id
@GeneratedValue
private Long id;
private 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 {
...
}
여기서는 Inheritance type를 TABLE_PER_CLASS로 지정한다.
장점
- 서브 타입을 구분해서 처리할 때 효과적이다.
- 각각의 자식 테이블이 존재하여, NOT NULL 인 컬럼 속성 활용 가능
단점
- 여러 자식 테이블을 함께 조인할 때 조회 성능이 떨어짐(UNION 사용 필요)
- 자식 테이블을 통합한 쿼리가 어렵다.
제일 추천하지 않는 방식의 상속 관계 표현방식이다.
@MappedSuperclass
부모 클래스를 테이블과 매핑하지 않고 상속 받는 자식 테이블에 매핑 정보만 제공하고자 한다면 @MappedSuperclass annotation을 활용한다.
즉, 부모 클래스는 테이블로 생성되지 않고, 단순히 매핑정보만 전달하게 된다.
위와 같이 부모 클래스는 존재하지만 테이블로는 생성하지 않는다.
@MappedSuperclass
public abstract class BaseEntity{
@Id
@GeneratedValue
private Long id;
private String name;
}
@Entity
public class Member extends BaseEntity{
private String email;
}
@Entity
public class Seller extends BaseEntity{
private String shopName;
}
@AttributeOverride annotation을 이용해서 매핑정보를 수정할 수 있다.
```java
@AttributeOverride(name="id", column=@Column(name="MEMBER_ID"))
@AttributeOverrides({
@AttributeOverride(name="id", column=@Column(name="MEMBER_ID"))
@AttributeOverride(name="name", column=@Column(name="MEMBER_NAME"))
})
위처럼, 상속받은 매핑정보를 수정할 수 있다.
부모 클래스는 실제 테이블과는 매핑되지 않기 때문에, em.find() 나 JPQL을 이용한 조회를 할 수 없다.
@MappedSuperclass를 이용하면 등록일자, 수정일자, 등록자, 수정자와 같이 여러 엔티티에 공통으로 사용되는 속성들을 효과적으로 관리할 수 있다.
복합키와 식별 관계 매핑
식별 관계
부모의 기본키를 받아서 자식의 기본키와 외래키로 이용하는 것으로, 부모의 기본키를 통해 자식이 식별된다.
비식별 관계
부모의 기본키를 자식의 외래키로만 활용된다.
-
필수적 비식별관계: 외래키에 NULL 값이 존재할 수 없다, 항상 연관관계가 형성되어 있어야함
-
선택적 비식별관계: 외래키에 NULL 값 저장 가능
복합키을 이용한 비식별 관계 매핑
JPA에서는 복합키를 기본키로 활용하기 위해서는 반드시 식별자 클래스를 통해서 키로 지정해야한다.
public class MemberProductId implements Serializable{
private String member; //MemberProduct의 member
private String product //MemberProduct의 product
@Override
public boolean equals(Object o){
}
@Override
public int hashCode(){
}
}
영속성 컨텍스트는 키가 서로 동등한지 확인하기 위해 hashCode와 equals 메소드를 이용해서 검사하는데, 키 값이 2개이상이면 위 처럼 클래스를 통한 메소드를 구현해서 제공해야한다.
인스터스가 다르더라도 키 값이 일치하는 경우에는 같은 인스턴스임이라고 반환하게 된다.
아래의 예제를 통해 알아보자
@IdClass
부모 클래스
@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;
...
}
@IdClass를 이용해서 식별자 클래스를 명시하고, @Id를 이용해서 복합키에 포함되는 기본키를 지정해준다.
식별자 클래스
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){...}
@Override
public int hashCode(){...}
}
부모 엔티티 저장
Parent parent=new Parent();
parent.setId("myId1");
parent.setId("myId2");
parent.setName("parentName");
em.persist(parent);
JPA로 저장할 때는 ParentId 클래스를 이용하지 않는데, 이는 영속성 컨텍스트에 저장되기 전에 내부적으로 자동으로 Parent.id1,Parent.id2를 이용해서 식별자 클래스로 키를 만들어서 영속성 컨테스트의 키로 활용한다.
부모 엔티티 조회
ParentId parentId=new ParentId("myId1","myId2");
Parent parent=em.find(Parent.class, parentId)
조회를 할때는 식별자 클래스를 이용해서 복합키를 제공해서 조회할 수 있다.
자식 클래스
@Entity
public class Child {
@Id
private Long id;
@ManyToOne
@JoinColumns({ // 속성이랑 컬럼이랑 같으면, 후자는 생략 가능
@JoinColumn(name="parent_id1", referencedColumnName = "id1"),
@JoinColumn(name="parent_id2", referencedColumnName = "id2")
})
private Parent parent;
}
자식 클래스를 보면 외래키가 복합적으로 이루어져있기 때문에, JoinColumns을 이용해서 여러개의 JoinColumn을 명시해줘야 한다.
EmbeddedId
부모 클래스
@Entity
public class Parent {
@EmbeddedId
private ParendId id;
private String name;
...
}
@IdClass를 이용해서 식별자 클래스를 명시하고, @Id를 이용해서 복합키에 포함되는 기본키를 지정해준다.
식별자 클래스
@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){...}
@Override
public int hashCode(){...}
}
부모 엔티티 저장
Parent parent=new Parent();
ParentId parentId=new ParentId("myId1","myId2");
parent.setId(parentId);
parent.setName("parentName");
em.persist(parent);
저장할 때, 식별자 클래스를 이용해서 직접 복합키를 만들어서 지정한다.
부모 엔티티 조회
ParentId parentId=new ParentId("myId1","myId2");
Parent parent=em.find(Parent.class, parentId)
조회를 할때는 식별자 클래스를 이용해서 복합키를 제공해서 조회할 수 있다.
자식 클래스
@Entity
public class Child {
@Id
private Long id;
@ManyToOne
@JoinColumns({ // 속성이랑 컬럼이랑 같으면, 후자는 생략 가능
@JoinColumn(name="parent_id1", referencedColumnName = "id1"),
@JoinColumn(name="parent_id2", referencedColumnName = "id2")
})
private Parent parent;
}
@IdClass vs @Embedded 방식
둘의 차이는 상당히 미묘하다. @IdClass는 조금 더 DB 친화적인 방식이고, @EmbeddedId방식은 객체에 친화적인 방식이다.
복합키를 이용한 식별관계 매핑
@IdClass
Parent Class
@Entity
public class Parent{
@Id
@Column(name="PARENT_ID")
private String id;
private String name;
}
Child, ChildId class
@Entity
@IdClass(ChildId.class)
public class Child{
@Id
@ManyToOne
@JoinColumn(name="PARENT_ID")
public Parent parent;
@Id
@Column(name="CHILD_ID")
private String id2;
private String name;
}
public class ChildId implements Serializable{
private String parent;
private String child;
public boolean equals(Object o){};
public int hashCode(){};
}
GrandChild, GrandChildId class
@Entity
@IdClass(GrandChildId.class)
public class GrandChild{
@Id
@ManyToOne
@JoinColumns({
@JoinColumn(name="PARENT_ID"),
@JoinColumn(name="CHILD_ID")
})
public Child child;
@Id
@Column(name="GRANDCHILD_ID")
private String id;
private String name;
}
public class GrandChildId implements Serializable{
private ChildId child;
private String id;
public boolean equals(Object o){};
public int hashCode(){};
}
@EmbeddedId
Parent Class
@Entity
public class Parent{
@Id
@Column(name="PARENT_ID")
private String id;
private String name;
}
Child, ChildId class
@Entity
public class Child{
@EmbeddedId
private ChildId id;
@MapsId("parentId")
@ManyToOne
@JoinColumn(name="PARENT_ID")
public Parent parent;
private String name;
}
@Embeddable
public class ChildId implements Serializable{
private String parentId;
@Column(name="CHILD_ID")
private String id;
public boolean equals(Object o){};
public int hashCode(){};
}
@MapsId를 이용해서 식별자 클래스에 매핑되는 컬럼을 명시한다. 외래키로 사용되는 키를 기본키로 사용하고자 할때 사용되는 annotation이다.
GrandChild, GrandChildId class
@Entity
public class GrandChild{
@EmbeddedId
private GrandChildId id;
@MapsId("childId")
@ManyToOne
@JoinColumns({
@JoinColumn(name="PARENT_ID"),
@JoinColumn(name="CHILD_ID")
})
public Child child;
private String name;
}
@Embeddable
public class GrandChildId implements Serializable{
private ChildId childId;
@Column(name="GRANDCHILD_ID")
private String id;
public boolean equals(Object o){};
public int hashCode(){};
}
비식별 관계를 식별관계로 표현
위의 식별 관계로 표현한 예제를 아래 그림과 같은 비식별 관계로 변경해보자.
Parent Class
@Entity
public class Parent{
@Id
@GeneratedValue
@Column(name="PARENT_ID")
private Long id;
private String name;
}
Child class
@Entity
public class Child{
@Id
@GeneratedValue
@Column(name="CHILD_ID")
private Long id;
@ManyToOne
@JoinColumn(name="PARENT_ID")
public Parent parent;
private String name;
}
GrandChild class
@Entity
public class GrandChild{
@Id
@GeneratedValue
@Column(name="GRANDCHILD_ID")
private Long id;
@ManyToOne
@JoinColumn(name="CHILD_ID")
public Child child;
private String name;
}
비식별 관계로 표현하게 되면 위와 같이 구조가 매우 깔끔해진다.
1:1 식별관계
위와 같이 부모의 기본키를 받아 자식 클래스의 기본키와 외래키로 사용하고, 유일한 기본키만으로 활용할 때는 복합키가 아니므로 식별자 클래스를 이용하지 않아도 된다.
Board Class
@Entity
public class Board{
@Id
@GeneratedValue
@Column(name="BOARD_ID")
private Long id;
private String title;
@OneToOne(mappedBy="board")
private BoardDetail boardDetail;
}
BoardDetail Class
@Entity
public class BoardDetail{
@Id
private Long boardId;
@MapsId
@OneToOne
@JoinColumn(name="BOARD_ID")
private Board board;
private String content;
}
비식별 관계 vs 식별 관계
식별관계를 이용하게 되면 기본키를 복합키로 구성해야되며 이에 따라 식별자 클래스를 생성하게 되면서 구현의 복잡성이 추가된다. 반면 비식별 관계에서는 대리키를 이용한 방식으로 기본키 구성을 단순화 할 수 있다. 그래서 대부분의 경우 비식별 관계를 통한 구성이 더 효율적이다.
식별 관계를 이용하면 좋은 경우가 있는데,상위 테이블의 기본키를 하위 테이블의 기본키에 포함시켜 기본키 인덱스를 활용할 수 있다는 장점이 있지만, 대개 비식별 관계를 이용하는 것이 더 좋다.
Join Table
기존에 다대다 관계에서 연결 엔티티를 이용해서 테이블을 이용한 연관관계를 매핑했는데, 이와 비슷한 개념이다. 아래와 같이 두 테이블간의 연관관계를 표현하는 테이블을 생성하는 것이다.
기존에 일대일 조인 컬럼으로 표현한 연관관계를 조인 테이블을 이용해서 나타내보자.
One To One Join Table
@Entity
public class Parent {
@Id
@GeneratedValue
@Column(name = "PARENT_ID")
private long id;
private String name;
// 조인 컬럼
@OneToOne
@JoinColumn(name = "CHILD_ID")
private Child child;
// 조인 테이블
@OneToOne
@JoinTable(name = "PARENT_CHILD",
joinColumns = @JoinColumn(name = "PARENT_ID"), // Parent와 매핑할 외래 키
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")) // Child와 매핑할 외래 키
private Child child;
}
@Entity
public class Child {
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private long id;
//양방향으로 설정하고자 할때
@OneToOne(mappedBy="child")
private Parent parent;
}
One To Many 조인테이블
@Entity
public class Parent {
@Id
@GeneratedValue
@Column(name = "PARENT_ID")
private long id;
private String name;
// 조인 컬럼
@OneToOne
@JoinColumn(name = "CHILD_ID")
private Child child;
// 조인 테이블
@OneToMany
@JoinTable(name = "PARENT_CHILD",
joinColumns = @JoinColumn(name = "PARENT_ID"), // Parent와 매핑할 외래 키
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")) // Child와 매핑할 외래 키
private List<Child> childs=new ArrayList<>();
}
@Entity
public class Child {
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private long id;
}
Many To One Join Table
다대일은 일대다 연관관계에서 방향만 바꿔서 생각하면 된다.
@Entity
public class Parent {
@Id
@GeneratedValue
@Column(name = "PARENT_ID")
private long id;
private String name;
// 조인 컬럼
@OneToOne
@JoinColumn(name = "CHILD_ID")
private Child child;
// 조인 테이블
@OneToMany(mappedby="parent")
private List<Child> childs=new ArrayList<>();
}
@Entity
public class Child {
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private long id;
@JoinTable(name = "PARENT_CHILD",
joinColumns = @JoinColumn(name = "CHILD_ID"), // Child와 매핑할 외래 키
inverseJoinColumns = @JoinColumn(name = "PARENT_ID")) // Parent와 매핑할 외래 키
@ManyToOne(optional=false)
private Parent parent;
}
Many To Many Join Table
@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"), // Parent와 매핑할 외래 키
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")) // Child와 매핑할 외래 키
private List<Child> childs=new ArrayList<>();
}
@Entity
public class Child {
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private long id;
private String name;
}
Multiple Table
엔티티 하나에 대해 여러 테이블을 구성할 수 있다.
@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 속성 정보
options | Description |
---|---|
name | 매핑 다른 테이블의 이름 지정 |
PrimaryKeyJoinColumn | 다른 테이블의 기본 키 지정 |
@SecondaryTables을 이용해서 여러 개의 SecondaryTable을 지정할 수 있다.
@Column annotation에서 table을 명시하면 해당 테이블에 컬럼을 설정한다.
웬만하면 하나의 엔티티를 2개의 테이블로 구성하는 경우는 없도록 한다. 1대1 매핑 2개로 표현하는 것이 더 효율적인 방식이다.
References
book: 자바 ORM 표준 JPA 프로그래밍 -김영한 저
댓글남기기