고급 매핑


7.1 상속 관계 매핑

  • 관계형 데이터베이스에는 객체지향 언어에서 다루는 상속이라는 개념이 없음
  • 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑


조인 전략(Join Strategy)

  • 엔티티 각각을 모두 테이블로 만들고 조회할 때 조인 사용

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

 @Entity
 @DiscriminatorValue("B")
 @PrimaryKeyJoinColumn(name="BOOK_ID")
 public class Book extends Item {
      private String author;
      private String isbn;
      ...
 }
  • 장점
    • 테이블이 정규화된다.
    • 외래키 참조 무결성 제약조건을 활용할 수 있다.
    • 저장 공간을 효율적으로 사용한다.
  • 단점
    • 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.
    • 조회 쿼리가 복잡하다.
    • 데이터를 등록할 INSERT SQL을 두 번 실행한다.
  • 특징
    • JPA 표준 명세는 구분 컬럼을 사용하도록 하지만 하이버네이트를 포함한 몇몇 구현체는 구분 컬럼(@DiscriminatorColumn) 없이도 동작한다.



단일 테이블 전략(Single-Table Strategy)

  • 테이블을 하나만 사용해서 통합
  • 구분 컬럼으로 어떤 자식 데이터가 저장되었는지 구분
  • 자식 엔티티가 매핑한 컬럼은 모두 null 허용

 @Entity
 @Inheritance(strategy=InheritanceType.SINGLE_TABLE)
 @DiscriminatorColumn(name="DTYPE")
 public abstract class Item {
      @Id
      @GeneratedValue
      @Column(name="ITEM_ID")
      private Logn 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;
      ...
 }

 @Entity
 @DiscriminatorValue("B")
 public class Book extends Item {
      private String author;
      private String isbn;
      ...
 }
  • 장점
    • 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다.
    • 조회 쿼리가 단순하다.
  • 단점
    • 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
    • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 상황에 따라 조회 성능이 오히려 느려질 수 있다.
  • 특징
    • 구분 컬럼을 꼭 사용해야 한다.
    • @DiscriminatorValue를 지정하지 않으면 기본으로 엔티티 이름을 사용한다.



구현클래스마다 테이블 전략(Table-per-Concrete-Class Strategy)

  • 서브 타입마다 하나의 테이블을 만든다
  • 일반적으로 추천하지 않는 전략이다.

 @Entity
 @Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
 public abstract class Item {
      @Id
      @GeneratedValue
      @Column(name="ITEM_ID")
      private Logn id;
      ...
 }

 @Entity
 public class Album extends Item {
      private String artist;
      ...
 }

 @Entity
 public class Movie extends Item {
      private String director;
      private String actor;
      ...
 }

 @Entity
 public class Book extends Item {
      private String author;
      private String isbn;
      ...
 }
  • 장점
    • 서브 타입을 구분해서 처리할 때 효과적이다.
    • not null 제약 조건을 사용할 수 있다.
  • 단점
    • 여러 자식 테이블을 함께 조회할 때 성능이 느리다.
    • 자식 테이블을 통합해서 쿼리하기 어렵다.
  • 특징
    • 구분 컬럼을 사용하지 않는다.


7.2 @MappedSuperclass

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

  • BaseEntity에는 객체들이 주로 사용하는 공통 매핑 정보 정의
  • 자식 엔티티들은 상속을 통해 BaseEntity의 매핑 정보를 물려받음
  • BaseEntity는 테이블과 매핑할 필요가 없고 자식 엔티티에게 공통으로 사용되는 매핑 정보만 제공
  • @MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용할 수 없다.
  • 이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장한다.
@Entity
@Table(name = "mt_user", indexes = { @Index(name = "idx_us_created_dt", columnList = "created_dt")}, uniqueConstraints = @UniqueConstraint(name = "userid", columnNames = { "userid" }))
@Audited
@QueryEntity
@Getter
@Setter
@EqualsAndHashCode(callSuper = false, of = { "username" })
@ToString(exclude = { "company", "agent", "userOptions", "licenseUsers", "userSecurityAccessIps", "userSecurityAccessMacs",
        "relayWindows", "favoriteFiles", "favorites", "shortMessageUsers", "userLog", "adminLog", "partnerAdminLog", "feedbacks",
        "userPasswordLogs", "supportRequest", "guideImageFiles" })
@NoArgsConstructor
public class User extends BaseTraceableEntity<String> implements UserDetails {

    private static final long serialVersionUID = 5250825939158807177L;

    public static final String UPLOAD_DIR = "user_image";

    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    @Column(name = "urid", length = ColumnSizeConstants.UUID)
    private String id;

    @Column(name = "userid", nullable = false, length = ColumnSizeConstants.ID)
    private String username; // 아이디

    @ManyToOne(optional = true)
    @JoinColumn(name = "grid")
    @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
    private Group group;

    @ManyToOne(optional = false)
    @JoinColumn(name = "ccid", updatable = false)
    @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    private Company company; // 소속사

    ...

@SuppressWarnings("serial")
@MappedSuperclass()
@Audited
public abstract class BaseTraceableEntity<K extends Serializable> extends BaseTimestampEntity<K> {

    @Column(name = "created_by", insertable = true, updatable = false, length = ColumnSizeConstants.UUID)
    private String createdBy;


    @Column(name = "updated_by", insertable = true, updatable = true, length = ColumnSizeConstants.UUID)
    private String updatedBy;

    @Column(name = "created_by_nm", insertable = true, updatable = false, length = ColumnSizeConstants.FULL_NAME)
    private String createdByNm;


    @Column(name = "updated_by_nm", insertable = true, updatable = true, length = ColumnSizeConstants.FULL_NAME)
    private String updatedByNm;


    public BaseTraceableEntity(){
    }

    ...

@SuppressWarnings("serial")
@MappedSuperclass()
@Getter
@Setter
public abstract class BaseTimestampEntity<K extends Serializable> extends BaseEntity<K> {

    @Transient
    @XmlTransient
    @JsonIgnore
    private boolean disableAutoTrace;

    @Column(name = "updated_dt", insertable = true, updatable = true)
    private Date updatedDt;

    @Column(name = "created_dt", insertable = true, updatable = false)
    private Date createdDt;

}


부모로부터 물려받은 매핑 정보를 재정의하려면 @AttributeOverrides나 @AttributeOverride를 사용하고, 연관 관계를 재정의하려면 @AssociationOverrides나 @AssociationOverride를 사용한다.

@Entity
@AttributeOverrides({
          @AttributeOverride(name="created_by", column=@Column(name="CREATED_BY_ID")),
          @AttributeOverride(name="updated_by", column=@Column(name="UPDATED_BY_ID"))
     })
@Table(name = "mt_user", indexes = { @Index(name = "idx_us_created_dt", columnList = "created_dt")}, uniqueConstraints = @UniqueConstraint(name = "userid", columnNames = { "userid" }))
@Audited
@QueryEntity
@Getter
@Setter
@EqualsAndHashCode(callSuper = false, of = { "username" })
@ToString(exclude = { "company", "agent", "userOptions", "licenseUsers", "userSecurityAccessIps", "userSecurityAccessMacs",
        "relayWindows", "favoriteFiles", "favorites", "shortMessageUsers", "userLog", "adminLog", "partnerAdminLog", "feedbacks",
        "userPasswordLogs", "supportRequest", "guideImageFiles" })
@NoArgsConstructor
public class User extends BaseTraceableEntity<String> implements UserDetails {

    private static final long serialVersionUID = 5250825939158807177L;

    public static final String UPLOAD_DIR = "user_image";

    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    @Column(name = "urid", length = ColumnSizeConstants.UUID)
    private String id;

    @Column(name = "userid", nullable = false, length = ColumnSizeConstants.ID)
    private String username; // 아이디

    ...


7.3 복합키와 식별 관계 매핑

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

  • 식별관계(Identitying Relationship)

    • 부모 테이블의 기본키를 내려받아서 자식 테이블의 기본키 + 외래키로 사용하는 관계
  • 비식별관계(Non-Identifying Relationship)

    • 부모 테이블의 기본키를 받아서 자식 테이블의 외래키로만 사용하는 관계
    • 필수적 비식별 관계(Mandatory) : 외래키에 NULL을 허용하지 않는다.
    • 선택적 비식별 관계(Optional) : 외래키에 NULL을 허용한다.

데이터베이스 테이블을 설계할 때는 식별 관계나 비식별 관계 중 하나를 선택해야 한다. 최근에는 비식별 관계를 주로 사용하고, 꼭 필요한 곳에만 식별 관계를 사용하는 추세다. 비식별 관계에서도 선택적 비식별 관계는 NULL 허용으로 인해 아웃 조인을 걸어야 하므로 이너 조인만 하는 필수적 관계를 사용하는 것이 좋다.


비식별관계(Non-Identifying Relationship)

  • JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스를 만들어야 한다.
  • JPA는 복합키를 지원하기 위해 @IdClass와 @EnbeddedId 의 방법을 제공
    • @IdClass : 관계형 데이터베이스에 가까운 방법
    • @EmbeddedId : 객체지향에 가까운 방법


@IdClass

  • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
  • Serializable 인터페이스를 구현해야 한다.
  • equals, hashCode를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.
@Entity
@Table(name="rim_branch_product")
@IdClass(BranchProductId.class)
public class BranchProduct {

    @Id
    @Column(name="product_id")
    private String productId;

    @Id
    @Column(name="branch_code")
    private String branchCode;

    ...
}
     public class BranchProductId implements Serializable {
          private String productId;
          private String branchCode;

          public BranchProductId() { }

          public BranchProductId(String productId, String branchCode) {
               this.productId = productId;
               this.branchCode = branchCode;
          }

          @Override
          public boolean equals(Object o) { ... }

          @Override
          public int hashCode() { ... }
     }
     @Entity
     @Table(name="rim_branch_product_price")
     public class BranchProductPrice {
          @Id
          private String productPriceKey;

          @ManyToOne
          @JoinColumns({
               @JoinColumn(name="product_id", referencedColumnName="product_id"),
               @JoinColumn(name="branch_code", referencedColumnName="branch_code")
          })
          private BranchProduct branchProduct;
          ...
     }
BranchProduct branchProduct = new BranchProduct();
branchProduct.setProductId("REMOTECALL_STANDARD");
branchProduct.setBranchCode("KOREA");
branchProduct.setBranchProductName("리모트콜 스탠다드");
em.persist(branchProduct);
BranchProductId branchProductId = new BranchProductId("REMOTECALL_STANDARD", "KOREA");
BranchProduct branchProduct = em.find(BranchProductId.class, branchProductId);


@EmbeddedId

  • @EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본키를 직접 매핑한다.
  • @Embeddable 어노테이션을 붙여주어야 한다.
  • Serializable 인터페이스를 구현해야 한다.
  • equals, hashCode를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.
@Entity
@Table(name="rim_branch_product")
public class BranchProduct {

    @EmbeddedId
    private BranchProductId Id;

    ...
}
     @Embeddable
     public class BranchProductId implements Serializable {

        @Column(name="product_id")
        private String productId;

        @Column(name="branch_code")
        private String branchCode;

          public BranchProductId() { }

          public BranchProductId(String productId, String branchCode) {
               this.productId = productId;
               this.branchCode = branchCode;
          }

          @Override
          public boolean equals(Object o) { ... }

          @Override
          public int hashCode() { ... }
     }
     @Entity
     @Table(name="rim_branch_product_price")
     public class BranchProductPrice {
          @Id
          private String productPriceKey;

          @ManyToOne
          @JoinColumns({
               @JoinColumn(name="product_id", referencedColumnName="product_id"),
               @JoinColumn(name="branch_code", referencedColumnName="branch_code")
          })
          private BranchProduct branchProduct;
          ...
     }
BranchProduct branchProduct = new BranchProduct();
BranchProductId branchProductId = new BranchProductId("REMOTECALL_STANDARD", "KOREA");
branchProduct.setId(branchProductId);
branchProduct.setBranchProductName("리모트콜 스탠다드");
em.persist(branchProduct);
BranchProductId branchProductId = new BranchProductId("REMOTECALL_STANDARD", "KOREA");
BranchProduct branchProduct = em.find(BranchProductId.class, branchProductId);



식별관계(Identitying Relationship)

  • @IdClass나 @EmbeddedId를 사용해서 식별자를 매핑해야 한다.


@IdClass

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

  private String name;

}

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

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

  private String name;
}

@Data
public class ChildId implements Serializable {

  private String parent;
  private String childId;
}

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

  private String name;
}

@Data
public class GrandChildId implements Serializable{
  private ChildId child;
  private String id;
}


@EmbeddedId

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

  private String name;

}

@Entity
@Data
public class Child {

  @EmbeddedId
  private ChildId id;

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

  private String name;
}

@Data
@Embeddable
public class ChildId implements Serializable {

  private String parentId; //@MapsId("parentId") 로 매핑
  private String childId;
}

@Entity
@Data
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;
}

@Data
@Embeddable
public class GrandChildId implements Serializable{
  private ChildId childId; //@MapsId("childId") 로 매핑

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


데이터베이스 설계 관점에서 비식별 관계를 선호하는 이유

  • 식별 관계는 자식 테이블의 기본키 컬럼이 점점 늘어난다. 결국 조인할 때 SQL이 복잡해지고 기본키 인덱스가 불필요하게 커질 수 있다.
  • 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본키를 만들어야 하는 경우가 많다.
  • 비즈니스 요구 사항은 시간이 지남에 따라 변하기 마련인데, 식별 관계의 기본키는 비즈니스 의미가 있어 변경이 필요할 경우 자식 테이블도 변경해야 한다.
  • 비식별 관계보다 테이블 구조가 유연하지 못하다.


객체 지향 설계 관점에서 비식별 관계를 선호하는 이유

  • 복합키는 별도의 복합키 클래스를 만들어 사용해야 하기 때문에 키 매핑에 많은 노력이 필요하다.
  • 비식별 관계의 기본키는 @GenerateValue 등을 사용하여 생성하기 편리하다.


7.4 조인 테이블

데이터베이스 테이블의 연관 관계를 설계하는 방법은 크게 2가지 이다.

  • 조인 컬럼 사용(외래키)
  • 조인 테이블 사용(테이블 사용)


일대일 조인 테이블

@Entity
@Data
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
@Data
public class Child {

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

  private String name;

}


일대다 조인 테이블

일대다 단방향 조인 테이블

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

@Entity
@Data
public class Child {

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

  private String name;
}


다대일 조인 테이블

다대일 양방향 조인 테이블 매핑

@Entity
@Data
public class Parent {
  @Id
  @GeneratedValue
  @Column(name = "PARENT_ID")
  private Long id;

  private String name;

  @OneToMany(mappedBy = "parent")
  private List<Child> child = new ArrayList<>();
}

@Entity
@Data
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
@Data
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"),
    inverseJoinColumns = @JoinColumn(name = "CHILD_ID"))
  private List<Child> child = new ArrayList<>();

}

@Entity
@Data
public class Child {

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

  private String name;
}


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

잘 사용하지 않지만 @SecondaryTable을 사용하면 한 엔티티에 여러 테이블을 매핑할 수 있다.

results matching ""

    No results matching ""