JPA - 고급매핑(2)

DevSeoRex·2022년 11월 6일
2
post-thumbnail
post-custom-banner

@MappedSuperclass

부모 클래스는 테이블과 매핑하지 않고, 부모 클래스를 상속 받는 자식 클래스에게
매핑 정보만 제공하고 싶으면 @MappedSuperclass를 사용하면 된다.

@Entity는 실제 테이블과 매핑되지만 @MappedSuperclass는 실제 테이블과는 매핑되지 않는다.


회원과 판매자는 서로 관계가 없는 테이블과 엔티티다. 회원과 판매자의 공통 속성인 id,name을 정의한 부모 클래스를 생성하고 객체 상속 관게로 만들 수 있다.

@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 Seller extends BaseEntity {
	// ID 상속
    // NAME 상속
    private String shopName;
    ...
}
  • BaseEntity에는 공통 매핑 정보를 정의한다.
  • 자식 엔티티들(Member, Seller)은 상속을 통해 BaseEntity의 매핑 정보를 물려 받는다.
  • BaseEntity는 테이블과 매핑할 필요가 없다.
  • BaseEntity는 자식 엔티티에게 공통으로 사용되는 매핑 정보만 제공하면 된다.
  • BaseEntity는 @MappedSuperclass 애너테이션을 사용하면 된다.

자식 클래스에서의 매핑 정보 재정의

부모로부터 물려받은 매핑 정보를 재정의하거나, 연관관계를 재정의 하려면 다음과 같은 애너테이션을 사용해서 재정의 할 수 있다.

  • @AttributeOverrides , @AttributeOverride : 매핑 정보 재정의
  • @AssociationOverrides, @AssociationOverride : 연관관계 재정의
// 하나의 속성을 재정의
@Entity
@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID")
public class Member extends BaseEntity { ... }

// 둘 이상의 속성을 재정의
@Entity
@AttributeOverrides({
	@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID")),
    @AttributeOverride(name = "name" column = @Column(name = "MEMBER_NAME"))
})
public class Member extends BaseEntity { ... }

@MappedSuperclass의 특징과 주의할 점

  • 테이블과 매핑되지 않고 자식 클래스에 인테티의 매핑 정보를 상속하기 위해 사용한다.
  • @MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용할 수 없다.
  • 이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것이 좋다.
  • @MappedSuperclass를 사용하면 등록일자, 수정일자, 등록자 같은 여러 엔티티에서 공통으로 사용하는 속성을 보다 효과적으로 관리 가능하다.
  • 엔티티(@Entity)는 엔티티이거나 @MappedSuperclass로 지정한 클래스만 상속받을 수 있다.

복합 키와 식별관계 매핑

데이터베이스 테이블 사이에 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별 관계와 비식별 관계로 구분한다.

식별 관계(identifying RelationShip)

식별 관계는 부모 테이블의 기본키를 내려 받아서 자식 테이블의 기본 키 + 외래키로 사용하는 관계다.


식별관계는 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래키로 사용 관계다.

비식별관계(Non-identifying RelationShip)

비식별 관계는 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계다.

비식별 관계는 외래키에 NULL을 허용하는지에 따라 필수적 비식별 관계와 선택적 비식별 관계로 나뉜다.

  • 필수적 비식별 관계(Mandantory) : 외래 키에 NULL을 허용하지 않는다. 연관관계를 필수적으로 맺어야 한다.
  • 선택적 비식별 관계(Optional) : 외래 키에 NULL을 허용한다. 연관관계를 맺을지 말지 선택할 수 있다.

최근에는 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 추세다.
JPA는 식별관계와 비식별 관계를 모두 지원한다.

복합키 - 비식별 관계 매핑

기본 키를 구성하는 컬럼이 하나면 아래 코드와 같이 단순하게 매핑한다.

@Entity
public class Hello {
	@Id
    private String id;
}

둘 이상의 컬럼으로 구성된 복합 기본키는 아래와 같이 매핑하면 오류가 발생한다.

@Entity
public class Hello {
	@Id
    private String id1
    @Id
    private String id2;
}

JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스를 만들어야 한다.

  • JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용
  • 식별자를 구분하기 위해 equals와 hashcode를 사용해서 동등성을 비교
  • 식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들어야 함
  • 식별자 클래스에 equals와 hashcode를 구현해야 한다.
  • JPA는 복합 키를 지원하기 위해 2가지 방법을 제공한다
    • @EmbeddedId : 좀 더 객체지향에 가까운 방법
    • @IdClass : 관계형 데이터 베이스에 가까운 방법

@IdClass

PARENT 테이블은 기본 키를 PARENT_ID1, PARENT_ID2로 묶은 복합키로 구성하였다.

// @IdClass를 사용한 복합키 매핑 예제 - 부모 클래스
@Entity
@IdClass(ParentId.class)
public class Parent {

	@Id
    @Coulumn(name = "PARENT_ID1")
    private String id1; // ParentId.id1과 연결
    
    @Id
    @Coulumn(name = "PARENT_ID2)
    private String id2 // ParentId.id2 연결
    
    private String name;
    ...
}
// @IdClass를 사용한 식별자 클래스 지정예제
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() { ... }
}

@IdClass를 사용할때 식별자 클래스의 조건

  • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
  • Serializable 인터페이스를 구현해야 한다.
  • equals, Hashcode를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.
// 복합 키를 사용하는 엔티티를 저장하는 예제

Parent parent = new Parent();
parent.setId1("myId1");
parent.setId2("myId2");
parent.setNmae("parentName");
em.persist(parent);

em.persist( )를 호출하면 영속성 컨텍스트에 엔티티를 등록하기 직전에 내부에서 Parent.id1, Parent.id2 값을 사용해서 식별자 클래스인 ParentId를 생성하고 영속성 컨텍스트의 키로 사용하기 때문에, 식별자 클래스인 ParentId가 코드에서 보이지 않는다.

// 복합 키를 사용하여 조회하는 예제
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);
// 자식 클래스를 사용한 복합 키 매핑
@Entity
public class Child {
	@Id
    private String id;
    
    @ManyToOne
    @JoinColumns({
    	@JoinColumn(name = "PARENT_ID1",
        	referencedColumnName = "PARENT_ID1"),
        @JoinColumn(name = "PARENT_ID2",
        	referencedColumnName = "PARENT_ID2")
    })
    private Parent parent;
}

부모 테이블의 기본 키 컬럼이 복합 키이므로 자식 테이블의 외래 키도 복합 키다.
외래 키 매핑 시 여러 컬럼을 매핑해야 하므로 @JoinColumns 애너테이션을 사용하고,
각각의 외래 키 컬럼을 @JoinColumn으로 매핑한다.

@JoinColumn의 name 속성과 referencedColumnName 속성의 값이 같으면 referencedColumnName은 생략해도 된다

@EmbeddedId

@IdClass가 데이터베이스에 맞춘 모양이라면 @EmbededId는 좀 더 객체지향적인 방법이다.

@Entity
public class Parent {

	@EmbededId
    private ParentId id;
    
    private String name;
    ...
}

Parent 엔티티에서 식별자 클래스를 직접 사용하고 @EmbededId 애너테이션을 적어주면 된다.

@Embeddable
public class ParentId implements Serializable {
	
    @Column(name = "PARENT_ID1")
    private String id1;
    @Column(name = "PARENT_ID2")
    private String id2;
    
    // eqauls and hashCode 구현

}
  • @IdClass와는 다르게 @EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본 키를 직접 매핑한다.

@EmbeddedId를 적용한 식별자 클래스가 만족해야 하는 조건

  • @Embeddable 애너테이션을 붙여주어야 한다.
  • Serializable 인터페이스를 구현해야 한다.
  • equals, hashCode를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.
// @EmbeddedId를 사용하는 예제
Parent parent = new Parent();
ParentId parentId = new ParentId("myId1","myId2");
parent.setId(parentid);
parent.setName("parentName");
em.persist(parent);

복합 키와 equals( ), hashCode( )

복합 키는 equals()와 hashCode()를 필수로 구현해야 한다.

  • 영속성 컨텍스트는 엔티티의 식별자를 키로 사용해서 엔티티를 관리한다.
  • 식별자를 비교할 때 equals( )와 hashCode( )를 사용한다.
  • 식별자 객체의 동등성이 지켜지지 않으면 엔티티를 관리하는 데 심각한 문제가 발생한다.
    - 예상과 다른 엔티티가 조회되거나, 엔티티를 찾을 수 없는 문제가 발생할 수 있다.

@IdClass vs @EmbeddedId

@EmbeddedId가 @IdClass와 비교해서 더 객체지향적이고 중복도 없어서 좋아보이긴 하지만, 특정상황에 JPQL이 조금 더 길어질 수 있다.

// @EmbeddedId를 사용한 JPQL
em.createQuery("select p.id.id1, p.id.id2 from Parent p");

// @IdClass를 사용한 JPQL
em.createQuery("select p.id1, p.id2 from Parent p");

복합 키에는 @GeneratedValue를 사용할 수 없다. 복합 키를 구성하는 여러 컬럼 중 하나에도 사용할 수 없다.

복합 키 - 식별 관계 매핑

  • 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야 한다.
  • 자식 테이블은 @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
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;
    
    @Id @Column(name = "CHILD_ID")
    private String childId;
    
    private String name;
    ...
}

// 자식 ID
public class ChildId implements Serializable {
	
    private String parent; // Child.parent 매핑
    private String chlidId; // Child.childId 매핑
    
    // equals, hashCode
    ...
}

// 손자
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
	
    @Id
    @ManyToOne
    @JouinColumns({
    	@JoinColumn(name = "PARENT_ID"),
        @JoinColumn(name = "CHILD_ID")
    })
    private Child child;
    
    @Id @Column(name = "GRANDCHILD_ID")
    private String id;
    
    private String name;
    ...
}

// 손자 ID
public class GrandChildId implements Serializable {
	
    private ChildId child; 	// GrandChild.child 매핑
    private String id;		// GrandChild.id 매핑
    
    // equals, hashCode
    ...
}
  • 식별 관계는 기본 키와 외래 키를 같이 매핑해야 한다.
  • 식별자 매핑인 @Id와 연관관계 매핑인 @ManyToOne을 같이 사용하면 된다.

@EmbeddedId와 식별 관계

// 부모
@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") // ChildId.parentId 매핑
    @ManyToOne
    @JoinColum(name = "PARENT_ID")
    public Parent parent;
    
    private String name;
    ...
}

// 자식 ID
@Embeddable
public class ChildId implements Serializable {
	
    private String parentId; // @MapsId("parentId")로 매핑
    
    @Column(name = "CHILD_ID")
	private String id;
    
    // equals, 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;
    ...
}

// 손자 ID
@Embeddable
public class GrandChildId implements Serializable {
	
    private ChildId childId; // @MapsId("childId")로 매핑
    
    @Column(name = "GRANDCHILD_ID")
    private String id;
    
    // equals, hashCode
    ...
}
  • @EmbeddedId는 식별 관계로 사용할 연관관계의 속성에 @MapsId를 사용하면 된다.
  • @IdClass와 다른 점은 @Id 대신에 @MapsId를 사용했다는 점이 다르다.
  • @MapsId는 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻이다.
  • @MapsId의 속성 값은 @EmbeddedId를 사용한 식별자 클래스의 기본 키 필드에 작성하면 된다.

복합 키 - 비식별 관계로 구현

// 비식별 관계 매핑 예제

// 부모
@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;
    ...
}
  • 복합 키가 없으므로 복합 키 클래스를 만들지 않아도 된다.

일대일 식별 관계

  • 일대일 식별 관계는 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용한다.
  • 부모 테이블의 기본 키가 복합 키가 아니면 자식 테이블의 기본 키는 복합 키로 구성하지 않아도 된다.
// 일대일 식별 관계 매핑 예제

// 부모
@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;
    ...
}
  • 식별자가 단순히 컬럼 하나일 경우 @MapsId를 사용하고 속성 값은 비워두면 된다.
  • @MapsId는 @Id를 사용해서 식별자로 지정한 필드와 매핑된다.
// 일대일 식별 관계 저장 예제

public void save() {
	Board board = new Board();
    board.setTitle("제목");
    em.presist(board);
    
    BoardDetail boardDetail = new BoardDetail();
    boardDetail.setContent("내용");
    boardDetail.setBoard(board);
    em.persist(boardDetail);
    
}

식별, 비식별 관계의 장단점

식별 관계의 단점

  • 식별 관계는 부모 테이블의 기본 키를 자식테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어난다.
  • 조인 할때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
  • 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다.
  • 비즈니스 요구사항이 변경될 경우, 식별 관계의 자연 키 컬럼들이 자식에 손자까지 전파되면 변경하기 어렵다.
  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하기 때문에 비식별 관계보다 테이블 구조가 유연하지 못하다.
  • 일대일 관계를 제외하고 식별 관계는 2개 이상의 컬럼을 묶은 복합 기본키를 사용한다.
  • JPA에서 복합 키는 별도의 복합 키 클래스를 만들어서 사용해야 한다.

식별 관계의 장점

  • 기본 키 인덱스를 활용하기 좋다.
  • 특정 상황에 조인 없이 하위 테이블만으로 검색을 완료 할 수 있다.

부모 아이디가 A인 모든 자식 조회

SELECT * FROM CHILD
WHERE PARENT_ID = 'A';

부모 아이디가 A고 자식 아이디가 B인 자식 조회

SELECT * FROM CHILD
WHERE PARENT_ID = 'A' AND CHILD_ID = 'B';

식별관계가 가지는 장점도 분명히 있지만, 될 수 있으면 비식별 관계를 사용하고 기본 키는 Long 타입의 대리 키를 사용하는 것이 좋다.

대리 키를 사용하면 얻을 수 있는 이점

  • 대리키는 비즈니스와 아무 관련이 없기 때문에 비즈니스가 변경되어도 유연한 대처가 가능하다.
  • JPA는 @GeneratedValue를 통해 간편하게 대리 키를 생성 가능하다.
  • 식별자 컬럼이 하나여서 쉽게 매핑할 수 있다.

대리 키 컬럼에서 Integer 대신 Long을 쓰는 이유

  • Integer의 범위는 20억 정도면 끝나버리기 때문에 데이터를 많이 저장하면 문제가 발생 할 여지가 있다.
  • Long은 약 920경의 범위를 가지고 있으므로, 비교적 Integer에 비해 안전하다.

선택적 비식별 관계 vs 필수적 비식별 관계 무엇을 선택해야 할까?

  • 선택적 비식별 관계(Optional)은 NULL을 허용하므로, 조인할 때에 외부 조인을 사용해야 한다.
  • 필수적 관계(Mandantory)는 NOT NULL로 항상 관계가 있다는 것을 보장하므로 내부 조인만 사용해도 된다.

따라서 선택적 비식별 관계보다는 필수적 비식별 관계를 사용하는 것이 좋다.

출처 : 자바 ORM 표준 JPA 프로그래밍(에이콘, 김영한 저)

post-custom-banner

0개의 댓글