고급 매핑
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을 사용하면 한 엔티티에 여러 테이블을 매핑할 수 있다.