자바 ORM 표준 JPA 프로그래밍 - 기본편 챕터 7 정리

정종일·2023년 5월 25일
0

Spring

목록 보기
12/18

고급 매핑


  • 상속관계 매핑
  • @MappedSuperclass
  • 복합키와 식별 관계 매핑
  • 조인 테이블
  • 엔티티 하나에 여러 테이블 매핑

1. 상속 관계 매핑


관계형 DB는 객체지향 언어에서 다루는 상속 이라는 개념이 없다..!

대신 슈퍼타입 서브타입 관계 라는 모델링 기법이 객체의 상속 개념과 가장 유사하다

슈퍼타입 서브타입 논리 모델을 실제 물리모델인 테이블로 구현할 때는 아래와 같은 방법들이 있다

  1. 각각의 테이블로 변환
    • 각각을 모두 테이블로 만들고 조회할 때 조인을 사용
    • JPA에서의 조인 전략
  2. 통합 테이블로 변환
    • 테이블을 하나만 사용해서 통합
    • JPA에서의 단일테이블 전략
  3. 서브타입 테이블로 변환
    • 서브타입마다 하나의 테이블을 만듬
    • JPA에서의 구현 클래스마다 테이블 전략

조인 전략

자식 테이블이 부모테이블의 기본 키를 받아서 '기본 키+외래 키'로 사용하는 전략

객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없어 타입을 구분하는 컬럼을 추가해야한다.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {

		@Id
		@GeneratedValue
		@Column(name = "ITEM_ID")
		private Long id;

		...
}

@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;

		...
}
  1. @Inheritance(strategy = InheritanceType.JOINED)
    • 상속 매핑은 부모 클래스에 해당 어노테이션을 사용
    • 매핑전략을 조인전략으로 설정
  2. @DiscriminatorColumn(name = "DTYPE")
    • 부모 클래스에 구분 컬럼 지정
    • 부모 컬럼으로 자식 테이블을 구분
    • default = DTYPE
  3. @DiscriminatorValue("M")
    • 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
    • 영화 엔티티를 저장하면 구분 컬럼인 DTYPE에 M이 저장됨

자식 테이블의 기본 키 컬럼명을 변경하고 싶으면 @PrimaryKeyJoinColumn을 사용

@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID")
public class Book extends Item {

		private String author;
		private String isbn;		

		...
}

Book 테이블의 ITEM_ID 기본 키 컬럼명을 BOOK_ID로 변경하였다.

  • 장점 → 테이블 정규화 → 외래 키 참조 무결성 제약조건 활용 가능 → 저장공간 효율적 사용
  • 단점 → 조회시 Join이 많아 성능저하 → 조회 쿼리가 복잡 → 데이터를 등록할 INSERT SQL 두 번 실행
  • 특징 → JPA 표준 명세는 구분 컬럼을 사용하도록 하지만 하이버네이트를 포함한 몇몇 구현체는 구분컬럼 (@DiscriminatorValue) 없이도 동작
  • 관련 어노테이션 → @DiscriminatorValue , @PrimaryKeyJoinColumn , @DiscriminatorColumn

단일 테이블 전략

테이블을 하나만 사용하는 전략, 조회시 조인을 사용하지 않아 가장 빠르다

자식 엔티티가 매핑한 컬럼은 모두 nullable 해야한다

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {

		@Id
		@GeneratedValue
		@Column(name = "ITEM_ID")
		private Long id;

		...
}

@Entity
@DiscriminatorValue("A")
public class Album extends Item {	... }

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {	... }

@Entity
@DiscriminatorValue("B")
public class Book extends Item {	... }

테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수로 사용해야 함

  • 장점 → 조인이 필요없어 조회 성능 빠름 → 조회 쿼리가 단순
  • 단점 → 자식 엔티티가 매핑한 컬럼은 모두 nullable → 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있어 상황에 따라서는 조회 성능이 오히려 저하될 수 있음
  • 특징 → 구분컬럼 사용 필수 (@DiscriminatorColumn) → @DiscriminatorValue을 지정하지 않으면 기본 엔티티 이름 사용

구현 클래스마다 테이블 전략

자식 엔티티마다 테이블을 만들며 각각 필요한 컬럼이 모두 존재

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {

		@Id
		@GeneratedValue
		@Column(name = "ITEM_ID")
		private Long id;

		...
}

@Entity
public class Album extends Item {	... }

@Entity
public class Movie extends Item {	... }

@Entity
public class Book extends Item {	... }

구현 클래스마다 테이블 전략 사용, DB설계자와 ORM 전문가 둘 다 비추천하는 전략

  • 장점 → 서브 타입을 구분해서 처리할 때 효과적 → not null 제약조건 사용
  • 단점 → 여러 자식 테이블을 함께 조회할 때 성능이 느림 (SQL의 UNION을 사용) → 자식 테이블을 통합해서 쿼리하기 어려움
  • 특징 → 구분 컬럼 미사용

2. @MappedSuperclass

부모클래스는 테이블과 매핑하지 않고 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고 싶을 때 사용

@Entity 는 실제 테이블과 매핑, @MappedSuperclass 는 단순히 매핑정보를 상속

매핑정보를 재정의 : @AttributeOverrides , @AttributeOverride

연관관계를 재정의 : @AssociationOverrides , @AssociationOverride

@Entity
@AttributeOverrides({
			@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID")),
			@AttributeOverride(name = "name", column = @Column(name = "MEMBER_NAME"))
})
public class Member extends BaseEntity { ... }

둘 이상을 재정의하려면 위와 같이 하면 된다 !

@MappedSuperclass 특징

  • 테이블과 매핑되지 않고 자식 클래스에 엔티티 매핑 정보를 상속
  • 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용 불가
  • 추상 클래스로 만드는 것을 권장
✅ `@MappedSuperclass` 는 테이블과 관계가 없고 매핑 정보를 모아주는 역할을 할 뿐! 이를 사용하면 등록일자, 수정일자, 등록자, 수정자와 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있다 !

3. 복합 키와 식별 관계 매핑


식별 관계 vs 비식별 관계

  • 식별관계 : 부모 테이블의 기본 키를 내려받아 자식 테이블의 기본 키 + 외래 키로 사용
  • 비식별관계 : 부모 테이블의 기본 키를 받아 자식 테이블의 외래 키로만 사용
    • 필수적 비식별 관계 : 외래키에 NULL을 허용하지 않음, 연관관계를 필수로 맺어야함
    • 선택적 비식별 관계 : 외래키에 NULL을 허용, 연관관계를 맺을지 여부를 선택

복합 키 : 비식별 관계 매핑

🙄 JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용한다. 식별자를 구분하기 위해 `equals`와 `hashCode`를 사용해서 동등성 비교를 한다. 식별자 필드가 하나일 때는 보통 자바의 기본 타입을 사용하므로 문제가 없지만 식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 그곳에 `equals`와 `hashCode`를 구현해야 한다.

복합 키 지원 어노테이션

@IdClass : 관계형 데이터베이스에 가까운 방법

@EmbeddedId : 객체지향에 가까운 방법

@IdClass

@Entity
@IdClass(ParentId.class)
public class Parent {
		
		@Id
		@Column(name = "PARENT_ID1")
		private String id1;
	
		@Id
		@Column(name = "PARENT_ID2")
		private String id2;
	
		...
}
// 식별자 클래스
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) { ... }
		
		@Overrid
		public int hashCode() { ... }
}

식별자 클래스는 아래의 조건을 만족해야 한다

  • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야함 ex) Parent.id1, ParentId.id1 ... etc
  • Serializable 인터페이스를 구현해야 함
  • eqauls, hashCode를 구현해야 함
  • 기본 생성자가 있어야 함
  • 식별자 클래스는 public
Parent parent = new Parent();
parent.setId1("myId1");
parent.setId2("myId2");
parent.setName("parentName");
em.persist(parent);

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

// 복합 키 조회
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);

식별자 클래스인 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 어노테이션을 사용하고 각각의 외래 키 컬럼을 @JoinColuimn 으로 매핑

@JoinColumnname 속성과 referencedColumnName 속성의 값이 같으면 생략가능

@EmbeddedId

@Entity
@IdClass(ParentId.class)
public class Parent {
	
		@EmbeddedId
		private String id;
	
		...
}
// 식별자 클래스
@Embeddable    // jobda-database -> ThemeCompanyReivew에서 사용중인 어노테이션
public class ParentId implements Serializable {

		@Column(name = "PARENT_ID1")
		private String id1;

		@Column(name = "PARENT_ID2")
		private String id2;

		//equals and hashCode 구현
		...
}

@EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본 키를 직접 매핑하며 아래와 같은 조건을 만족해야 한다

  • @Embeddable 어노테이션을 붙여줘야 함
  • Serializable 인터페이스 구현해야 함
  • equals, hashCode 구현해야 함
  • 기본 생성자가 있어야 함
  • 식별자 클래스는 public
Parent parent = new Parent();
ParentId parentId = new ParentId("myId1", "myId2");
parent.setId(parentId);
parent.setName("parentName");
em.persist(parent);

식별자 클래스 parentId를 직접 생성해서 사용

ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);

식별자 클래스 parentId를 직접 사용

복합 키와 equals() , hashCode()

ParentId id1 = new parentId();
id1.setid1("myId1");
id1.setid2("myId2");

ParentId id2 = new parentId();
id2.setid1("myId1");
id2.setid2("myId2");

id1.equals(id2) -> ????

id1.equals(id2)는 참? 거짓?

  • equals()를 적절히 오버라이딩 했다면 참이겠지만 그 반대의 경우 거짓이다.
  • 자바의 모든 클래스는 기본으로 Object 클래스를 상속받는데 이 클래스가 제공하는 기본 equals()는 인스턴스 참조 값 비교인 == 비교이기 때문
✅ 영속성 컨텍스트는 엔티티의 식별자를 키로 사용해서 엔티티를 관리 식별자를 비교할 때 equals() 와 hashCode()를 사용 식별자 객체의 동등성이 지켜지지 않으면 예상과 다른 엔티티가 조회되거나 엔티티를 찾을 수 없는 등 영속성 컨텍스트가 엔티티를 관리하는 데 **심각한문제 발생**

@IdClass vs @EmbeddedId

EmbeddedId가 더 객체지향적이고 중복도 없어 좋아보이긴 하지만 특정 상황에따라 JPQL이 조금 더 길어질 수 있다.

✅ 복합 키에는 `@GenerateValue` 를 사용할 수 없다.

복합 키 : 식별 관계 매핑

식별관계에서 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야하므로 @IdClass@EmbeddedId를 사용해서 식별자를 매핑해야 한다

@IdClass와 식별 관계

식별 관계는 기본 키와 외래 키를 같이 매핑

//부모
@Entity
public class Parent {
		@Id
		@Column(name = "PARENT_ID")
		private String id;

		...
}
@Entity
@IdClass(ChildId.class)
public class Child {
		@Id
		@ManyToOne
		@JoinColumn(name = "PARENT_ID")
		public Parent parent;

		@Id
		@Column(name = "CHILD_ID")
		private String childId;

		...
}
// 자식 Id
public class ChildId implements Serializable {

		private String parent;
		private String childId;

		//equals and hashCode 구현
		...
}
// 손자
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {

		@Id
		@ManyToOne
		@JoinColumns({
					@JoinColumn(name = "PARENT_ID"),
					@JoinColumn(name = "CHILD_ID")
		})
		private Child child;

		@Id
		@Column(name = "GRANDCHILD_ID")
		private String id;
		
		...
}

Child 엔티티의 parent 필드를 보면 @Id로 기본키를 매핑하면서 @ManyToOne@JoinColumn 으로 외래키를 같이 매핑

@EmbeddedId와 식별 관계

//부모
@Entity
public class Parent {
		@Id
		@Column(name = "PARENT_ID")
		private String id;

		...
}
@Entity
@IdClass(ChildId.class)
public class Child {

		@EmbeddedId
		private ChildId id;

		@MapsId("parentId") // ChildId.parentId 매핑
		@ManyToOne
		@JoinColumn(name = "PARENT_ID")
		public Parent parent;

		...
}
// 자식 Id
@Embeddable
public class ChildId implements Serializable {

		private String parentId; // @MapsId("parentId")로 매핑
		
		@Column(name = "CHILD_ID")
		private String id;

		//equals and hashCode 구현
		...
}
// 손자
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {

		@EmbeddedId
		private GrandChildId id;		

		@MapsId("childId") // GrandChildId.childId 매핑
		@ManyToOne
		@JoinColumns({
					@JoinColumn(name = "PARENT_ID"),
					@JoinColumn(name = "CHILD_ID")
		})
		private Child child;

		...
}
// 손자 Id
@Embeddable
public class GrandChildId implements Serializable {

		private ChildId childId; // @MapsId("childId") 로 매핑

		@Column(name = "GRANDCHILD_ID")
		private String id;

		// eqauls, hashCode 

		...
}

식별 관계로 사용할 연관관계의 속성에 @MapsId를 사용하면 됨!

@IdClass와 다른점은 @MapsId를 사용한다는 것인데 외래키와 매핑한 연관관계를 기본키에도 매핑하겠다는 뜻

비식별 관계로 구현

식별관계에 비해 매핑도 쉽고 코드도 단순

복합키가 없으므로 복합키 클래스를 만들지 않아도 됨

일대일 비식별관계

// 부모
@Entity
public class Board {

		@Id
		@GeneratedValue
		@Column(name = "BOARD_ID")
		private Long id;

		@OneToOne(mappedBy = "board")
		private BoardDetail boardDetail;

		...
}
// 자식
@Entity
public class BoardDetail {

		@Id
		private Long boradId;

		@MapsId // BoardDetail.boardId
		@OneToOne
		@JoinColumn(name = "BOARD_ID")
		private Board board;

		...
}

BoardDetail 처럼 식별자가 단순히 컬럼 하나면 @MapsId 를 사용하고 속성값은 비워두면 됨

@MapsId@Id 를 사용해서 식별자로 지정한 BoardDetail.boardId와 매핑

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

식별 <<<< 비식별 선호 이유?

  • 식별관계는 자식테이블의 기본 키 컬럼이 점점늘어남. 조인할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커짐
  • 식별관계는 2개 이상의 컬럼을 합해서 복합 키를 만들어야 하는 경우가 많다
  • 기본 키로 비지니스 의미가 있는 자연 키 컬럼 조합이 많음, 추후 변경하기 힘들어짐
  • 식별관계는 부모 테이블의 기본 키를 자식 테이블의 기본키로 사용하므로 테이블 구조가 유연하지 않음

식별관계도 장점은 있다!

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

기본 키 인덱스를 PARENT_ID + CHILD_ID로 구성하면 별도의 인덱스를 생성할 필요 없이 기본 키 인덱스만 사용해도 된다 !

정리

  • ORM의 추천 방법은 비식별관계이며 기본 키는 Long타입을 추천
  • Integer 는 20억개가 한정, Long 타입은 920경까지 가능
  • 선택적 비식별관계(Nullable) 보다는 필수적 비식별 관계(Not null)

4. 조인 테이블


연관관계를 설정하는 방법

  • 조인 컬럼 사용 → 선택적 비식별 관계는 외래키에 null을 허용하므로 회원과 사물함을 조인할 때 외부 조인을 사용
  • 조인 테이블 사용 → 조인테이블이라는 별도의 테이블을 사용해서 연관관계를 관리 → 객체와 테이블을 매핑할 때 조인 컬럼은 @JoinColumn으로 매핑하고 조인 테이블은 @JoinTable로 매핑 → 조인 테이블은 주로 다대다 관계를 다대일 일대다 관례로 풀어내기 위해 사용, but 일대일, 일대다, 다대일 관계에서도 사용한다

일대일 조인 테이블

일대일 관계를 만들려면 조인 테이블의 외래 키 컬럼 각각에 총 2개의 유니크 제약조건을 걸어야 함

		@OneToOne
		@JoinTable(name = "PARENT_CHILD",
							joinColumns = @JoinColumn(name = "PARENT_ID"),
							inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
		)
		private Child child;
		
		...
}
  • name = 매핑할 조인 테이블 이름
  • joinColumns = 현재 엔티티를 참조하는 외래 키
  • inverseJoinColumns = 반대 방향 엔티티를 참조하는 외래 키
// 양방향 매핑을 하고싶다면 
public class Child {
		
		...

		@OneToOne(mappedBy = "child")
		private Parent parent;
}

일대다 조인 테이블

일대다 관계를 만들려면 조인 테이블의 컬럼 중 다(N)와 관련된 컬럼인 CHILD_ID에 유니크 제약조건을 걸어야 함
어노테이션만 @OneToMany 방법은 동일

대일 조인 테이블

다대일은 일대다에서 방향만 반대
어노테이션만 @ManyToOne, 조인 테이블 모양은 일대다와 같다

다대다 조인 테이블

다대다 관계를 만들려면 조인 테이블의 두 컬럼을 합해서 복합 유니크 제약조건을 걸어야함

어노테이션만 @ManyToMany , 방법 동일

✅ 조인 테이블에 컬럼을 추가하면 @JoinTable 전략 사용 불가 대신 새로운 엔티티를 만들어서 조인 테이블과 매핑

5. 엔티티 하나에 여러 테이블 매핑


@SecondaryTable 을 사용하여 여러테이블 매핑이 가능

@Entity
@Table(name = "BOARD")
@SecondaryTable(name = "BOARD_DETAIL",
		pkJoinColumns = @PrimaryKeyJoinColumn(name = "BOARD_DETAIL_ID"))
public class Board {

		...

		@Column(table = "BOARD_DETAIL")
		private String content;
}
  • @SecondaryTable.name : 매팅할 다른 테이블의 이름
  • @SecondaryTable.pkJoinColumns : 매핑할 다른 테이블의 기본 키 컬럼 속성
profile
제어할 수 없는 것에 의지하지 말자

0개의 댓글