티스토리 뷰
😼 고급 매핑
상속 관계 매핑
🤔 객체지향언어는 상속의 개념이 존재
관계형 db는??? 없다... 비스무리하게 구현을 해보자
❗슈퍼타입 서브타입 관계 모델링 기법
객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑
물리 모델인 테이블로 구성이 될 때는 3가지 방법을 택할 수 있다.
- 각각의 테이블로 변환 : join 전략
- 통합 테이블로 변환 : 하나의 테이블에서 관리 dtype 같은 구분자로 구분
- 서브타입 하나당 테이블을 하나씩 생성
알아둘 annotation
- @Inheritance : 상속 매핑은 부모 클래스에 @inheritance를 사용해야한다.
- @DiscriminatorColumn : 부모 클래스에 구분 칼럼을 지정한다. 이 컬럼으로 저장된 자식 테이블을 구분할 수 있다.
- @DiscriminatorValue : 자식클래스에 적용, 엔티티에 저장할 때 구분 컬럼에 입력할 값을 지정한다.
조인전략
조인 전략은 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로
사용하는 전략이다.
🖥 join 전략
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id
@Column(name = "ItemId")
private Long id;
private String name;
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
private String artist;
}
@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BookId") // BOOK의 pk 칼럼 이름 재정의
public class Book extends Item {
private String author;
}
👍 장점
- 테이블의 정규화
- 외래 키 참조 무결성 제약조건 활용 가능
- 저장 공간의 효율성
👎 단점
- 조회 할때 조인이 많이 사용 성능 저하의 우려
- 조회 쿼리 복잡
- 데이터 등록할때 insert 2번 실행
조인 전략에서의 조회 쿼리는 자동으로 left join 이 반영된다.
단일 테이블 전략
이름 그대로 테이블을 하나만 사용, 그리고 구분 칼럼(기본값 : DTYPE) 으로 어떤 자식데이터가 저장되었는지 구분
🖥 단일 테이블 (SingleTable 전략)
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id
@Column(name = "ItemId")
private Long id;
private String name;
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
private String artist;
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
private String author;
}
👍 장점
- 조인이 필요 없어 조회 성능이 빠르다
- 조회 쿼리가 단순
👎 단점
- 자식 엔티티가 매핑한 컬럼은 모두 nullable 해야함
- 단인 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.
- 구분 칼럼을 반드시 사용해야 함
구현 클래스마다 테이블 전략
쿠현 클래스마다 테이블 전략은 자식 엔티티마다 테이블을 만들고 각각에 필요한 컬럼이 모두 있다 (superClass의 컬럼들 또한 계속 다있다.)
👎 너무 구닥다리 전략이다. 사용하지말자.
🚀 공통 속성 관리 @MappedSuperclass
🤔 비슷한 Entity 끼리 (관심사가 비슷한...)와는 달리 어떤 공통속성 (예를 들면 log를 위한 생성, 수정 시점 컬럼 같은) 이 있고
그들을 관리하고 싶다면 @MappedSuperclass 를 사용하자
프로젝트를 하던와중 이미지 관리 엔티티를 MappedSuperclass 기반으로 만든적이 있는데 아주 쉽게 객체지향을 지킬수 있었다.
@MappedSuperclass는 실제 테이블과는 매핑되지 않는다 단순히 매핑 정보를 상속할 목적으로만 사용된다.
❗ 직접 생성할 일이 없으므로 추상클래스로 만드는 것을 권장한다. (물론 엔티티매니저로 이 엔티티를 find 할 수 없는 것 또한 당연하다.)
해당 내용을 더 확장한 spring 어노테이션을 보자
필요 개념 : @EntityListener, 엔티티의 생명주기
EntityListener를 사용하여 엔티티 생명 주기에 따른 동작을 구현 할 수 있다.
👍 spring data Jpa 에서는 간단히 AuditingEntityListener.class 를 지원해 귀찮음을 덜어준다.
코드로 살펴보자
🖥 EntityLister를 적용한 MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity extends BaseTimeEntity{
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
// @CreatedBy, @LastModifiedBy, @CreatedDate 는 AuditingEntityListener 가 지원한다.
그외에 직접 커스터마이징 하고싶을 경우
🖥 커스터마이징하고 싶을 때
public class MyEntityListener {
/**
* 들어오는 타입 (앤티티) 가 확실할 경우
* 즉, 이 리스너를 하나의 엔티티에서만 쓸경우
*/
@PostLoad
public void PostLoad(Member obj){
System.out.println("PostLoad obj = "+obj);
}
@PrePersist
public void prePersist(Member obj){
System.out.println("prePersist"+obj);
}
/**
* 다음과 같이 모를경우엔 obj로 할 수 도있다.
*/
@PreUpdate
public void PreUpdate(Object obj){
System.out.println("PreUpdate");
}
@PreRemove
public void PreRemove (Object obj){
System.out.println("PreRemove ");
}
@PostPersist
public void PostPersist (Object obj){
System.out.println("PostPersist ");
}
@PostUpdate
public void PostUpdate (Object obj){
System.out.println("PostUpdate ");
}
@PostRemove
public void PostRemove(Object obj){
System.out.println("PostRemove ");
}
}
@EntityListeners(MyEntityListener.class)
public class Member extends BaseEntity {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
private int age;
}
로그를 찍어볼 때 자주 사용되는 내용이다!
복합 키와 식별 관계 매핑
데이터베이스 테이블 사이 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별 관계와
비식별 관계로 구분
식별관계
자식테이블에서 부모테이블의 기본키를 기본키(복합키) + 외래키로 사용하는 관계
비식별 관계
자식테이블에서 부모테이블의 기본키를 외래키로만 사용하는 관계
- 필수적 비식별 관계
외래키에 NULL 을 허용하지 않는다. 연관관계가 필수적이다 - 선택적 비식별 관계
외래키에 NULL을 허용한다. 연관관계를 맺을지 안맺을지 선택가능
복합키 사용하기
🖥 복합키 오류 발생
@Entity
public class Example {
@Id
private String id1;
@Id
private String id2; // 실행 시점에 매핑 예외 발생!
}
전에도 말했지만 JPA는 엔티티관리를 (영속성 컨택스트 관리를 위해) 엔티티의 식별자로 동등성을 비교한다
키 속성이 하나였을 때는 자바의 기본타입을 사용해 문제가 없었으나 @Id 가 2개가 되면 당연히 오류가 날 수밖에 없다.
(동등성 비교 실패로 인한 오류)
JPA에서는 이러한 복합키를 위한 어노테이션을 지원한다.
- @IdClass
- @EmbeddedId
위에서도 말했지만 식별자로 동등성을 비교하기위해 현재 Id 들을 묶어 놓고 equals와 hashcode를
구현해 놓아야한다.
@IdClass
코드로 살펴보자
🖥 복합키를 위한 어노테이션 @IdClass
// 부모 클래스
@Entity
@IdClass(ParentId.class)
public class Parent{
@Id
@Column(name = "PARENT_ID1")
private String id1; // ParentId.id1과 연결
@Id
@Column(name = "PARENT_ID2")
private String id2; // ParentId.id2와 연결
private String name;
}
// 식별자 클래스 Parent PK 들을 한묶음으로 관리해야한다. JPA 는 이쪽을 사용해서 비교
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 이어야 한다.
EntityManager에서의 사용
🖥 @IdClass + EntityManager
// 엔티티 저장
Parent parent = new Parent();
parent.setId1("id1"); // 식별자
parent.setId2("id2"); // 식별자
parent.setName("parentName");
em.persist(parent);
// 엔티티 조회
ParentId parentId = new ParentId("id1", "id2");
Parent parent = em.find(Parent.class, parentId);
@EmbeddedId
🖥 @EmbeddedId 의 사용
@Entity
public class Parent {
@EmbeddedId
private ParentId id;
private String name;
}
@Embeddable // EmbeddedType 사용할 때 필요한 어노테이션 값타입에서 등장
public class ParentId implements Serializable {
@Column(name = "PARENT_ID1")
private String id1;
@Column(name = "PARENT_ID2")
private String id2;
// equals and hashCode 구현
}
조건
- @Embeddable 어노테이션을 붙여줘야 한다.
- Serializable 인터페이스 구현해야 한다.
- equals, hashCode 구현
- 기본 생성자
- 식별자 클래스는 public
EntityManager에서의 사용
🖥 @Embeddable + EntityManager
// 엔티티 저장
Parent parent = new Parent();
parent.setId1("id1"); // 식별자
parent.setId2("id2"); // 식별자
parent.setName("parentName");
em.persist(parent);
// 엔티티 조회
ParentId parentId = new ParentId("id1", "id2");
Parent parent = em.find(Parent.class, parentId);
🤔 왜 equals와 hashCode?
기본 Object를 상속받아 생기는 클래스는 기본 인스턴스 참조 값 비교인 == 비교를 한다
(주소 값 비교/ 참조 값 비교) 그러므로 JPA 가 동등성 비교를 할 때 사용하는 equals와
hashCode를 재정의 하여서 엔티티의 동등성 비교가 잘 되도록 해야한다.
참고로 복합키에는 @GenerateValue를 사용할 수 없다.
✅ 복합키를 사용하여 식별관계 매핑 해보기 (비식별 포함)
🖥 @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 childId; // Child.childId 매핑
// equals, hashCode
...
}
// 손자
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
@Id // 비식별 관계시 @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;
}
// 손자 ID
public class GrandChildId implements Serializable{
private ChildId child; // GrandChild.child 매핑
private String id; // GrandChild.id 매핑
// equals, hashCode
...
}
🖥 @EmbeddedId + 식별관계 매핑
// 부모
@Entity
public class Parent {
@Id
@Column(name = "PARENT_ID")
private String id;
private String name;
...
}
// 자식
@Entity
public class Child {
@EmbeddedId
private ChildId childId;
@MapsId("parentId") // ChildId.parentId 매핑
@ManyToOne
@JoinColumn(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 매핑 @Id 대신 사용
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID"),
@JoinColumn(name = "CHILD_ID")
})
public 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 를 사용해 엔티티가 하나의 Id 로 관리되는 것처럼 보인다.
- @IdClass는 키가 여러개라서 코드도 길어지고 한눈이 보이기 쉽지 않으나 외래키 관리나 Db 컬럼관리
가 편하다.
비식별 관계로의 전환
식별 관계를 비식별 관계로 바꾸면 식별 관계의 복합 키를 사용한 코드에 비해 매핑도 쉽고
코드가 상당히 단순해지는 장점이 있다. (복합키를 사용하지 않는다)
🖥 자연키를 사용하는 예시
// 부모
@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;
...
}
비식별 관계 VS 식별 관계
식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 늘어난다.
식별 관계는 2개이상의 칼럼을 합해서 복합 기본 키를 만들어야한는 경우가 많다
즉 기본 키로 의미가 있는 자연 키 칼럼을 조합한다.
=> 해당 내용들의 경우 자식 테이블이 부모 테이블의 키들을 가지고 있기 때문에 비지니스 요구
사항이 변경될 경우 쉽게 뜯어고칠 수 없다. (구조적으로 유연하지 못하다)
물론 식별 관계가 가지는 장점도 존재한다.
- 기본키 인덱스를 활용하기 좋다
- 상위 테이블들의 기본 키 칼럼을 자식, 손자 테이블들이 가지고 있으므로 특정상황에 조인 없이
하위 테이블만으로 검색을 완료 할 수 있다.
하지만 결국에는 ORM을 사용 하게 될시에 되도록 비식별 관계를 사용하고 기본 키는 Long 타입
의 대리키를 사용 할 것을 권장
대리키는 비지니스와 아무 관련이 없기 때문에 비지니스 변경시 유연한 대처가 가능하다.
즉 Long + @GenerateValue 조합을 사용하자
🚀 테이블 연관 관계의 표현
DB에서 테이블의 연관관계를 설계하는 방법은 크게 2가지
- 조인 칼럼(외래키)를 사용
- 조인 테이블 사용
조인 칼럼
테이블 간에 조인 칼럼이라고 부르는 외래키를 사용하여 관리
JPA 에서는 @JoinColumn 을 사용하여 관리
외래 키에 null 허용 여부에 따라 선택적/ 필수적으로 나뉨
조인 테이블
조인 테이블이라는 별도의 테이블을 사용하여 연관관계를 관리
@JoinTable 을 사용해서 매핑
주로 다대다 관계를 풀어낼때 사용하는데 대부분 다대다의 경우에는 3개의 엔티티로
풀어내는 편이 좋다
🔑 일대일 조인 테이블
조인 테이블에 유니크 제약조건 각각 걸어야 한다.
🖥 일대일 조인 테이블
// 부모
@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;
...
// 양방향 매핑 시
// @OneToOne(mappedBy="child")
// private Parent parent;
}
일대다 조인 테이블
🖥 일대다 조인 테이블
// 부모
@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;
...
}
다대일 조인 테이블
🖥 다대일 조인 테이블
@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") // 반대방향 엔티티를 참조하는 외래 키
)
private Parent parent;
...
}
다대다 조인 테이블
🖥 다대다 조인 테이블
// 부모
@Entity
public class Parent {
@Id
@GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@ManyToMany(mappedBy = "parent")
@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;
...
}
조인 테이블 정리
조인 테이블을 할 때는 거의 다대다를 일대다 다대일로 푸는 경우인데 이 또한
조인 테이블 자체를 엔티티로 뽑아서 따로 가져가는 것이 유연한 어플리케이션을 개발 할
수 있게 된다. 조인 테이블의 사용을 지양하자
'Spring > JPA' 카테고리의 다른 글
JPA - 값 타입 (0) | 2022.08.23 |
---|---|
프록시와 연관관계 관리 (0) | 2022.08.23 |
JPA - 연관관계 이해하기 (0) | 2022.08.03 |
JPA - 엔티티 매핑 (0) | 2022.08.03 |
JPA - 영속성 컨텍스트 (2) | 2022.08.03 |
- Total
- Today
- Yesterday