고급 매핑

상속관계 매핑

객체 지향형 프로그래밍에서는 아래와 같은 객체 간 상속을 지원한다.

class_inheritance_model

RDBMS에서 상속 모델을 구현하기 위해 3가지 방식을 지원하는데, 1:1, 싱글, 슈퍼 + 서브타입 방식이 존재한다.

각가의 방식으로 상속관계를 생성할 수 있고, 부모 클래슨 abstract, 자식 클래스는 이를 구현하는 방식으로 자바에서는 설계하면 된다.

1:1

join_strategy_inheritance

흔히, 조인전략이라고 말하는 이 방식은 부모 클래스, 자식 클래스에 대해 모두 테이블로 만들어서 기본키 + 외래키를 이용한 조인방식으로 상속관계를 표현한다.

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 컬럼명 변경 가능

장점

  1. 테이블이 정규화됨
  2. 외래키 참조 무결성 제약조건 활용(조인을 통한 조회)
  3. 저장공간이 효율적이다

단점

  1. 조인 성능이 안 좋다(매번 많은 조인이 필요하기 때문)
  2. 조회가 복잡하다(모든 테이블이 다 분리되어 있기 때문)
  3. INSERT 문을 부모 클래스, 자식 클래스 두번에 걸쳐 진행된다.

Single Table

single_table_inheritance

부모 클래스와 자식 테이블을 하나의 테이블로 구성하고, 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. 조회 성능이 가장 좋다(테이블 개수가 1개 이므로)
  2. 조회 쿼리가 간단해진다.

단점

  1. 자식 테이블과 관련된 모든 컬럼이 한 테이블에 포함되므로 NULL 값인 컬럼이 많이 존재한다.
  2. 오히려, 컬럼이 많아 지면서 조회 성능이 떨어질 수도 있다.

SuperType + SingleType

super_single_inheritance

자식 테이블을 각각 만들어 주며, 각각의 자식 테이블에 부모 테이블 관련 컬럼을 추가해주는 방식이다.

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로 지정한다.

장점

  1. 서브 타입을 구분해서 처리할 때 효과적이다.
  2. 각각의 자식 테이블이 존재하여, NOT NULL 인 컬럼 속성 활용 가능

단점

  1. 여러 자식 테이블을 함께 조인할 때 조회 성능이 떨어짐(UNION 사용 필요)
  2. 자식 테이블을 통합한 쿼리가 어렵다.

제일 추천하지 않는 방식의 상속 관계 표현방식이다.

@MappedSuperclass

부모 클래스를 테이블과 매핑하지 않고 상속 받는 자식 테이블에 매핑 정보만 제공하고자 한다면 @MappedSuperclass annotation을 활용한다.

즉, 부모 클래스는 테이블로 생성되지 않고, 단순히 매핑정보만 전달하게 된다.

mapped_superclass

위와 같이 부모 클래스는 존재하지만 테이블로는 생성하지 않는다.

@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를 이용하면 등록일자, 수정일자, 등록자, 수정자와 같이 여러 엔티티에 공통으로 사용되는 속성들을 효과적으로 관리할 수 있다.

복합키와 식별 관계 매핑

식별 관계

identifying_relationship

부모의 기본키를 받아서 자식의 기본키와 외래키로 이용하는 것으로, 부모의 기본키를 통해 자식이 식별된다.

비식별 관계

non_identifying_relationship

부모의 기본키를 자식의 외래키로만 활용된다.

  • 필수적 비식별관계: 외래키에 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개이상이면 위 처럼 클래스를 통한 메소드를 구현해서 제공해야한다.

인스터스가 다르더라도 키 값이 일치하는 경우에는 같은 인스턴스임이라고 반환하게 된다.

아래의 예제를 통해 알아보자

multikey

@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방식은 객체에 친화적인 방식이다.

복합키를 이용한 식별관계 매핑

identifying_relationship2

@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(){};
}

비식별 관계를 식별관계로 표현

위의 식별 관계로 표현한 예제를 아래 그림과 같은 비식별 관계로 변경해보자.

non_identifying_relationship2

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 식별관계

one_to_one_identifying_relationship

위와 같이 부모의 기본키를 받아 자식 클래스의 기본키와 외래키로 사용하고, 유일한 기본키만으로 활용할 때는 복합키가 아니므로 식별자 클래스를 이용하지 않아도 된다.

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

기존에 다대다 관계에서 연결 엔티티를 이용해서 테이블을 이용한 연관관계를 매핑했는데, 이와 비슷한 개념이다. 아래와 같이 두 테이블간의 연관관계를 표현하는 테이블을 생성하는 것이다. join_table

기존에 일대일 조인 컬럼으로 표현한 연관관계를 조인 테이블을 이용해서 나타내보자.

One To One Join Table

one_to_one_jointable

@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 조인테이블

one_to_many_jointable

@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

many_to_many_jointable

@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

엔티티 하나에 대해 여러 테이블을 구성할 수 있다.

multiple_tables

@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 프로그래밍 -김영한 저

book_link

태그: ,

카테고리:

업데이트:

댓글남기기