티스토리 뷰

Spring/JPA

고급 매핑

땅속 디그다 2022. 8. 23. 11:07

😼 고급 매핑

상속 관계 매핑

🤔 객체지향언어는 상속의 개념이 존재

관계형 db는??? 없다... 비스무리하게 구현을 해보자

❗슈퍼타입 서브타입 관계 모델링 기법
객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑

 

물리 모델인 테이블로 구성이 될 때는 3가지 방법을 택할 수 있다.

  1. 각각의 테이블로 변환 : join 전략
  2. 통합 테이블로 변환 : 하나의 테이블에서 관리 dtype 같은 구분자로 구분
  3. 서브타입 하나당 테이블을 하나씩 생성

알아둘 annotation

  1. @Inheritance : 상속 매핑은 부모 클래스에 @inheritance를 사용해야한다.
  2. @DiscriminatorColumn : 부모 클래스에 구분 칼럼을 지정한다. 이 컬럼으로 저장된 자식 테이블을 구분할 수 있다.
  3. @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;
}

👍 장점

  1. 테이블의 정규화
  2. 외래 키 참조 무결성 제약조건 활용 가능
  3. 저장 공간의 효율성

👎 단점

  1. 조회 할때 조인이 많이 사용 성능 저하의 우려
  2. 조회 쿼리 복잡
  3. 데이터 등록할때 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;
}

👍 장점

  1. 조인이 필요 없어 조회 성능이 빠르다
  2. 조회 쿼리가 단순

👎 단점

  1. 자식 엔티티가 매핑한 컬럼은 모두 nullable 해야함
  2. 단인 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.
  3. 구분 칼럼을 반드시 사용해야 함

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

쿠현 클래스마다 테이블 전략은 자식 엔티티마다 테이블을 만들고 각각에 필요한 컬럼이 모두 있다 (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 를 사용할 때의 조건

  1. 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야한다.
  2. 식별자 클래스는 @Serializable 인터페이스를 구현해야 한다.
  3. equals, hashCode를 구현해야 한다.
  4. 기본 생성자가 필요
  5. 식별자 클래스는 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 구현
}

조건

  1. @Embeddable 어노테이션을 붙여줘야 한다.
  2. Serializable 인터페이스 구현해야 한다.
  3. equals, hashCode 구현
  4. 기본 생성자
  5. 식별자 클래스는 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
    ...
}

🤔 차이점 (나의 생각)

  1. 임베디드 타입은 @EmbeddedId 를 사용해 엔티티가 하나의 Id 로 관리되는 것처럼 보인다.
  2. @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개이상의 칼럼을 합해서 복합 기본 키를 만들어야한는 경우가 많다

즉 기본 키로 의미가 있는 자연 키 칼럼을 조합한다.

=> 해당 내용들의 경우 자식 테이블이 부모 테이블의 키들을 가지고 있기 때문에 비지니스 요구
사항이 변경될 경우 쉽게 뜯어고칠 수 없다. (구조적으로 유연하지 못하다)

물론 식별 관계가 가지는 장점도 존재한다.

  1. 기본키 인덱스를 활용하기 좋다
  2. 상위 테이블들의 기본 키 칼럼을 자식, 손자 테이블들이 가지고 있으므로 특정상황에 조인 없이
    하위 테이블만으로 검색을 완료 할 수 있다.

 

하지만 결국에는 ORM을 사용 하게 될시에 되도록 비식별 관계를 사용하고 기본 키는 Long 타입
의 대리키를 사용 할 것을 권장

대리키는 비지니스와 아무 관련이 없기 때문에 비지니스 변경시 유연한 대처가 가능하다.

즉 Long + @GenerateValue 조합을 사용하자


🚀 테이블 연관 관계의 표현

DB에서 테이블의 연관관계를 설계하는 방법은 크게 2가지

  1. 조인 칼럼(외래키)를 사용
  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
댓글
11-08 10:33
Total
Today
Yesterday
링크