고급 매핑
7. 고급 매핑
7.1 상속 관계 매핑
- 관계형 데이터베이스에는 상속 개념이 없다. 대신 슈퍼타입 서브타입 관계라는 모델링 기법이 상속과 유사하다.
- 상속 관계 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것을 말한다.
- 슈퍼타입 서브타입 논리 모델을 실제 물리 모델인 테이블로 구현하는 방법
- 각각의 테이블로 변환: Item, Album, Movie, Book을 모두 테이블로 만들고 조회시 조인 사용
- 통합 테이블로 변환: 하나의 통합 테이블을 만들어 사용
- 서브타입 테이블로 변환: 서브타입인 Album, Movie, Book 테이블을 만들어 사용
7.1.1 조인 전략
- 엔티티 각각을 모두 테이블로 만들고, 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다.
- ITEM 테이블에 타입을 구분하기 위해 DTYPE 컬럼을 구분 컬럼으로 사용한다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "dtype")
@Getter @Setter
public abstract class Item {
@Id @GeneratedValue
@Column(name = "item_id")
private Long id;
private String name;
private String price;
}
@Entity
@DiscriminatorValue("A")
@Getter @Setter
public class Album extends Item {
private String artist;
}
@Entity
@DiscriminatorValue("B")
@Getter @Setter
public class Book extends Item {
private String author;
private String isbn;
}
@Entity
@DiscriminatorValue("M")
@Getter @Setter
public class Movie extends Item {
private String director;
private String actor;
}
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Book book = new Book();
book.setName("자바 ORM 표준 JPA 프로그래밍");
book.setAuthor("김영한");
book.setPrice("40000");
book.setIsbn("1234");
em.persist(book);
Album album = new Album();
album.setName("NEXT EPISODE");
album.setArtist("악뮤");
album.setPrice("10000");
em.persist(album);
em.flush();
em.clear();
Book foundBook = em.find(Book.class, book.getId());
Album foundAlbum = em.find(Album.class, album.getId());
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
insert into Item (name, price, dtype, item_id) values (?, ?, 'B', ?)
insert into Book (author, isbn, item_id) values (?, ?, ?)
insert into Item (name, price, dtype, item_id) values (?, ?, 'A', ?)
insert into Album (artist, item_id) values (?, ?)
select book.item_id, item.name, item.price, book.author, book.isbn
from Book book
inner join Item item on book.item_id = item.item_id
where book.item_id=?
select album.item_id, item.name, item.price, album.artist
from Album album
inner join Item item on album.item_id = item.item_id
where album.item_id=?
- @Inheritance(strategy = InheritanceType.JOINED) 어노테이션으로 매핑 전략을 지정한다.
- @DiscriminatorColumn(name = “dtype”) 어노테이션으로 부모 클래스에 구분 컬럼을 지정한다.
- @DiscriminatorValue(“A”) 어노테이션으로 자식 클래스에 구분 컬럼에 입력할 값을 지정한다.
-
자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용한다. @PrimaryKeyJoinColumn 으로 변경 가능하다.
- 장점
- 테이블이 정규화된다.
- 외래 키 참조 무결성 제약조건을 활용할 수 있다.
- 저장공간을 효율적으로 사용한다.
- 단점
- 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.
- 조회 쿼리가 복잡하다.
- 데이터 등록시 INSERT SQL을 두 번 실행한다.
7.1.2 단일 테이블 전략
- 테이블을 하나만 사용하고 구분 컬럼으로 구분한다.
- 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다.
- 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
@Getter @Setter
public abstract class Item {
@Id @GeneratedValue
@Column(name = "item_id")
private Long id;
private String name;
private String price;
}
insert into Item (name, price, author, isbn, dtype, item_id) values (?, ?, ?, ?, 'B', ?)
insert into Item (name, price, artist, dtype, item_id) values (?, ?, ?, 'A', ?)
select book.item_id, book.name, book.price, book.author, book.isbn
from Item book
where book.item_id=? and book.dtype='B'
select album.item_id, album.name, album.price, album.artist
from Item album
where album.item_id=? and album.dtype='A'
- 장점
- 조회 성능이 빠르다.
- 조회 쿼리가 단순하다.
- 단점
- 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 상황에 따라서 조회 성능이 오히려 느려질 수 있다.
- 특징
- 구분 컬럼을 꼭 사용해야 한다.
7.1.3 구현 클래스마다 테이블 전략
- 자식 엔티티마다 테이블을 만든다.
- 자식 테이블 각각에 필요한 컬럼이 모두 있다.
insert into Book (name, price, author, isbn, item_id) values (?, ?, ?, ?, ?)
insert into Album (name, price, artist, item_id) values (?, ?, ?, ?)
select item.item_id, item.name, item.price, item.artist, item.author, item.isbn, item.actor, item.director, item.clazz
from
(
select item_id, name, price, artist, null as author, null as isbn, null as actor, null as director, 1 as clazz
from Album
union all
select item_id, name, price, null as artist, author, isbn, null as actor, null as director, 2 as clazz
from Book
union all
select item_id, name, price, null as artist, null as author, null as isbn,a ctor, director, 3 as clazz
from Movie
) item
- 장점
- 서브 타입을 구분해서 처리할 떄 효과적이다.
- not null 제약조건을 사용할 수 있다.
- 단점
- 여러 자식 테이블을 함께 조회할 때 성능이 느리다. (SQL에 UNION 사용)
- 자식 테이블을 통합해서 쿼리하기 어렵다.
- 특징
- 구분 컬럼을 사용하지 않는다.
7.2 @MappedSuperclass
- 부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속받는 자식에게 매핑 정보만 제공한다.
@MappedSuperclass
public abstract class BaseEntity {
@Id @GeneratedValue
private Long id;
private String name;
}
@Entity
@Getter @Setter
public class Member extends BaseEntity {
private String email;
}
@Entity
@Getter @Setter
public class Seller extends BaseEntity {
private String shopName;
}
- 부모로부터 받은 매핑 정보를 재정의하려면 @AttributeOverrides나 @AttributeOverride를 사용한다.
- 연관관계를 재정의하려면 @AssociationOverride나 @AssociationOverride를 사용한다.
@Entity
@Getter @Setter
@AttributeOverrides({@AttributeOverride(name = "id", column = @Column(name = "member_id")),
@AttributeOverride(name = "name", column = @Column(name = "member_name"))
})
public class Member extends BaseEntity {
private String email;
}
- @MappedSuperclass로 지정한 클래스는 테이블과 매핑되지 않는다. 자식 클래스에 매핑 정보만 상속하기 위해 사용한다.
- @MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용할 수 없다.
- @MappedSuperclass로 지정한 클래스는 추상 클래스로 만드는 것을 권장한다.
7.3 복합 키와 식별 관계 매핑
7.3.1 식별 관계 vs 비식별 관계
식별 관계
- 부모 테이블의 기본 키를 자식 테이블의 기본 키 + 외래 키로 사용하는 관계를 말한다.
- 자식 테이블의 기본 키는 CHILD_ID + PARENT_ID 이고, 외래 키는 PARENT_ID 이다.
비식별 관계
- 부모 테이블의 기본 키를 자식 테이블의 외래 키로만 사용하는 관계를 말한다.
- 자식 테이블의 기본 키는 CHILD_ID 이고, 외래 키는 PARENT_ID 이다.
- 필수적 비식별 관계(Mandatory): 외래 키에 NULL을 허용하지 않는다. 연관관계를 필수적으로 맺어야 한다.
- 선택적 비식별 관계(Optional): 외래 키에 NULL을 허용한다. 연관관계를 선택할 수 있다.
7.3.2 복합 키: 비식별 관계 매핑
- 기본 키를 구성하는 컬럼이 하나일 때, 아래처럼 단순하게 매핑한다.
@Entity
public class Member {
@Id
private Long id;
}
- 둘 이상의 컬럼으로 구성된 복합 기본 키는 별도의 식별자 클래스를 만들어야 한다.
- JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용한다.
- 자바 기본 타입의 식별자 필드가 하나일 경우 문제가 없다.
- 식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 equals와 hashCode를 구현해야 한다.
- JPA는 복합 키를 지원하기 위해 @IdClass, @EmbeddedId 2가지 방법을 제공한다.
@IdClass
- 아래 테이블에서 PARENT와 CHILD는 비식별 관계이고, PARENT는 복합 기본 키를 사용한다.
@Entity
@IdClass(ParentId.class)
@Getter @Setter
public class Parent {
@Id
@Column(name = "parent_id1")
private Long id1;
@Id
@Column(name = "parent_id2")
private Long id2;
private String name;
}
@Getter @Setter
public class ParentId implements Serializable {
private Long id1;
private Long id2;
public ParentId() {
}
public ParentId(Long id1, Long id2) {
this.id1 = id1;
this.id2 = id2;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ParentId parentId = (ParentId) o;
return Objects.equals(id1, parentId.id1) && Objects.equals(id2, parentId.id2);
}
@Override
public int hashCode() {
return Objects.hash(id1, id2);
}
}
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Parent parent = new Parent();
parent.setId1(1L);
parent.setId2(5L);
parent.setName("parent");
em.persist(parent);
em.flush();
em.clear();
ParentId parentId = new ParentId(1L, 5L);
Parent foundParent = em.find(Parent.class, parentId);
System.out.println("foundParent.getName() = " + foundParent.getName());
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
- @IdClass 사용시 식별자 클래스는 다음 조건을 만족해야 한다.
- 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
- 즉, Parent 클래스의 필드 id1, id2와 ParentId 클래스의 필드 id1, id2는 일치해야 한다.
- Serializable 인터페이스를 구현해야 한다.
- equals, hashCode를 구현해야 한다.
- 기본 생성자가 있어야 한다.
- 식별자 클래스는 public이어야 한다.
@Entity
@Getter @Setter
public class Child {
@Id
private Long id;
@ManyToOne
@JoinColumns({@JoinColumn(name = "p_id1", referencedColumnName = "parent_id1"),
@JoinColumn(name = "p_id2", referencedColumnName = "parent_id2")})
private Parent parent;
}
- 부모 테이블의 기본 키가 복합 키 이므로 자식 테이블의 외래 키도 복합 키로 설정한다.
- @JoinColumns 어노테이션을 사용하여 각각의 외래 키 컬럼을 @JoinColumn으로 매핑한다.
- @JoinColumn의 name 속성은 외래 키 컬럼 이름, referencedColumnName 속성은 참조하는 테이블의 컬럼 이름이다.
@EmbeddedId
- @IdClass는 데이터베이스에 맞춘 방법이고, @EmbeddedId는 좀 더 객체지향적인 방법이다.
- Parent 엔티티에서 식별자 클래스를 직접 사용하고, @EmbeddedId 어노테이션을 선언한다.
- 식별자 클래스에서 기본 키를 매핑한다.
@Entity
@Getter @Setter
public class Parent {
@EmbeddedId
private ParentId id;
private String name;
}
@Getter @Setter
@Embeddable
public class ParentId implements Serializable {
@Column(name = "parent_id1")
private Long id1;
@Column(name = "parent_id2")
private Long id2;
public ParentId() {
}
public ParentId(Long id1, Long id2) {
this.id1 = id1;
this.id2 = id2;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ParentId parentId = (ParentId) o;
return Objects.equals(id1, parentId.id1) && Objects.equals(id2, parentId.id2);
}
@Override
public int hashCode() {
return Objects.hash(id1, id2);
}
}
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Parent parent = new Parent();
ParentId parentId = new ParentId(1L, 5L);
parent.setId(parentId);
parent.setName("parent");
em.persist(parent);
Child child = new Child();
child.setId(3L);
child.setParent(parent);
em.persist(child);
em.flush();
em.clear();
ParentId findParentId = new ParentId(1L, 5L);
Parent foundParent = em.find(Parent.class, findParentId);
System.out.println("foundParent.getName() = " + foundParent.getName());
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
- @EmbeddedId를 적용한 식별자 클래스는 다음 조건을 만족해야 한다.
- @Embeddable 어노테이션을 붙여주어야 한다.
- Serializable 인터페이스를 구현해야 한다.
- equals, hashCode를 구현해야 한다.
- 기본 생성자가 있어야 한다.
- 식별자 클래스는 public이어야 한다.
복합 키와 equals(), hashCode()
- 복합 키는 equals(), hashCode()를 필수로 구현해야 한다.
- 자바의 모든 클래스는 기본적으로 Object 클래스를 상속받는데, Object 클래스의 equals()는 == 비교, 즉 동일성 비교를 한다.
- 영속성 컨텍스트는 엔티티의 식별자를 키로 사용해서 엔티티를 관리하므로, 식별자 객체의 동등성 비교가 구현되어야 한다.
- 따라서 복합 키는 equals(), hashCode()가 동등성 비교가 되도록 구현해야 한다.
@IdClass vs @EmbeddedId
- @EmbeddedId가 @IdClass보다 좀 더 객체지향적이다.
- 왜냐하면 @EmbeddedId에서는 Parent 클래스에서 ParentId 필드를 선언해서 사용하기 때문이다.
- 하지만 특정 상황에서 @EmbeddedId는 JPQL이 좀 더 길어질 수 있다.
List<Object[]> parentIds = em.createQuery("select p.id.id1, p.id.id2 from Parent p").getResultList(); // @EmbeddedId
List<Object[]> parentIds = em.createQuery("select p.id1, p.id2 from Parent p").getResultList(); // @IdClass
복합 키에는 @GeneratedValue를 사용할 수 없다. 복합 키를 구성하는 여러 컬럼 중 하나에도 사용할 수 없다.
7.3.3 복합 키: 식별 관계 매핑
- 아래 테이블은 부모, 자식, 손자까지 계속 기본 키를 전달하는 식별관계이다.
- 식별 관계에서 자식 테이블은 기본 키를 구성할 때, 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야 한다.
@IdClass와 식별 관계
@Entity
@Getter @Setter
public class Parent {
@Id
@Column(name = "parent_id")
private String id;
private String name;
}
@Entity
@IdClass(ChildId.class)
@Getter @Setter
public class Child {
@Id
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
@Id
@Column(name = "child_id")
private String childId;
private String name;
}
@Getter @Setter
public class ChildId implements Serializable {
private String parent; // Child 클래스의 parent 필드
private String childId; // Child 클래스의 childId 필드
public ChildId() {
}
public ChildId(String parent, String childId) {
this.parent = parent;
this.childId = childId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ChildId childId1 = (ChildId) o;
return Objects.equals(getParent(), childId1.getParent()) && Objects.equals(getChildId(), childId1.getChildId());
}
@Override
public int hashCode() {
return Objects.hash(getParent(), getChildId());
}
}
@Entity
@IdClass(GrandChildId.class)
@Getter @Setter
public class GrandChild {
@Id
@ManyToOne
@JoinColumns({@JoinColumn(name = "p_id", referencedColumnName = "parent_id"),
@JoinColumn(name = "c_id", referencedColumnName = "child_id")})
private Child child;
@Id
@Column(name = "grandchild_id")
private String id;
private String name;
}
public class GrandChildId implements Serializable {
private ChildId child; // GrandChild 클래스의 child 필드, ChildId 타입으로 선언
private String id; // GrandChild 클래스의 id 필드
public GrandChildId() {
}
public GrandChildId(ChildId child, String id) {
this.child = child;
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GrandChildId that = (GrandChildId) o;
return Objects.equals(child, that.child) && Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(child, id);
}
}
@EmbeddedId와 식별 관계
@Entity
@Getter @Setter
public class Parent {
@Id
@Column(name = "parent_id")
private String id;
private String name;
}
@Entity
@Getter @Setter
public class Child {
@EmbeddedId
private ChildId id;
@MapsId("parentId") // ChildId 클래스의 parentId 매핑
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
private String name;
}
@Embeddable
@Getter @Setter
public class ChildId implements Serializable {
private String parentId; // @MapsId("parendId")
@Column(name = "child_id")
private String id;
public ChildId() {
}
public ChildId(String parentId, String id) {
this.parentId = parentId;
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ChildId childId = (ChildId) o;
return Objects.equals(getParentId(), childId.getParentId()) && Objects.equals(getId(), childId.getId());
}
@Override
public int hashCode() {
return Objects.hash(getParentId(), getId());
}
}
@Entity
@Getter @Setter
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;
}
@Embeddable
@Getter @Setter
public class GrandChildId implements Serializable {
private ChildId childId; // @MapsId("childId")
@Column(name = "grandchild_id")
private String id;
public GrandChildId() {
}
public GrandChildId(ChildId childId, String id) {
this.childId = childId;
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GrandChildId that = (GrandChildId) o;
return Objects.equals(getChildId(), that.getChildId()) && Objects.equals(getId(), that.getId());
}
@Override
public int hashCode() {
return Objects.hash(getChildId(), getId());
}
}
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Parent parent = new Parent();
parent.setId("parent");
parent.setName("parent");
em.persist(parent);
ChildId childId = new ChildId("parent", "child");
Child child = new Child();
child.setId(childId);
child.setParent(parent);
child.setName("child");
em.persist(child);
GrandChildId grandChildId = new GrandChildId(childId,"grandchild");
GrandChild grandChild = new GrandChild();
grandChild.setId(grandChildId);
grandChild.setChild(child);
grandChild.setName("grandchild");
em.persist(grandChild);
em.flush();
em.clear();
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
7.3.4 비식별 관계로 구현
@Entity
@Getter @Setter
public class Parent {
@Id @GeneratedValue
@Column(name = "parent_id")
private Long id;
private String name;
}
@Entity
@Getter @Setter
public class Child {
@Id @GeneratedValue
@Column(name = "child_id")
private Long id;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
private String name;
}
@Entity
@Getter @Setter
public class GrandChild {
@Id @GeneratedValue
@Column(name = "grandchild_id")
private Long id;
@ManyToOne
@JoinColumn(name = "child_id")
private Child child;
private String name;
}
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Parent parent = new Parent();
parent.setName("parent");
em.persist(parent);
Child child = new Child();
child.setParent(parent);
child.setName("child");
em.persist(child);
GrandChild grandChild = new GrandChild();
grandChild.setChild(child);
grandChild.setName("grandchild");
em.persist(grandChild);
em.flush();
em.clear();
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
7.3.5 일대일 식별 관계
- 일대일 식별 관계는 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용한다.
- 부모 테이블의 기본 키가 복합 키가 아니면 자식 테이블의 기본 키는 복합 키로 구성하지 않아도 된다.
@Entity
@Getter @Setter
public class Board {
@Id @GeneratedValue
@Column(name = "board_id")
private Long id;
private String title;
@OneToOne(mappedBy = "board")
private BoardDetail boardDetail;
}
@Entity
@Getter @Setter
public class BoardDetail {
@Id
private Long boardId;
@MapsId // BoardDetail.boardId 매핑
@OneToOne
@JoinColumn(name = "board_id")
private Board board;
private String content;
}
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Board board = new Board();
board.setTitle("title");
em.persist(board);
BoardDetail boardDetail = new BoardDetail();
boardDetail.setContent("content");
boardDetail.setBoard(board);
em.persist(boardDetail);
em.flush();
em.clear();
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
7.3.6 식별, 비식별 관계의 장단점
- 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파한다.
- 자식 테이블의 기본 키 컬럼이 점점 늘어난다.
- 예를 들어 부모 테이블은 기본 키 컬럼이 1개, 자식 테이블은 기본 키 컬럼이 2개, 손자 테이블은 기본 키 컬럼이 3개로 늘어난다.
- 조인할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
- 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다.
- 식별 관계는 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많다.
- 비식별 관계는 기본 키로 비즈니스와 관계 없는 대리 키를 주로 사용한다.
- 비즈니스 요구사항은 시간이 지남에 따라 변화하는데, 식별 관계의 자연 키 컬럼들이 자식, 손자까지 전파되면 변경하기 힘들다.
- 식별 관계는 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하므로 비식별 관계보다 테이블 구조가 유연하지 못하다.
- 일대일 관계를 제외하고 식별 관계는 2개 이상의 컬럼을 묶은 복합 기본 키를 사용한다. 식별자 클래스를 만들어서 사용해야 한다.
- 비식별 관계의 기본 키는 주로 대리 키를 사용하는데 @GeneratedValue 처럼 대리 키를 생성하기 위한 편리한 방법이 제공된다.
7.4 조인 테이블
- 데이터베이스의 테이블 연관관계를 설계하는 방법은 크게 2가지이다.
- 조인 컬럼 사용(외래 키)
- 조인 테이블 사용(테이블 사용)
- 조인 컬럼
- MEMBER와 LOCKER사이에 연관관계가 없을 경우 LOCKER_ID 외래 키에 null을 입력해두어야 한다. (선택적 비식별 관계)
- 선택적 비식별 관계이므로 조인시 외부 조인을 사용해야 한다.
- 조인 테이블
- 조인 테이블에서 MEMBER, LOCKER 외래 키를 관리하므로 MEMBER, LOCKER에는 외래 키 컬럼이 없다.
- MEMBER, LOCKER사이에 연관관계 추가시 MEMBER_LOCKER 테이블에만 값을 추가하면 된다.
- 기본은 조인 컬럼을 사용하고 필요하면 조인 테이블을 사용하는게 좋다.
7.4.1 일대일 조인 테이블
- 일대일 관계를 만드려면, 조인 테이블의 외래 키 컬럼 각각에 유니크 제약조건을 지정해야 한다.
@Entity
@Getter @Setter
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
@Getter @Setter
public class Child {
@Id @GeneratedValue
@Column(name = "child_id")
private Long id;
private String name;
@OneToOne(mappedBy = "child")
private Parent parent;
}
- @JoinTable 속성
- name: 매핑할 조인 테이블 이름
- joinColumns: 현재 엔티티를 참조하는 외래 키
- inverseJoinColumns: 반대방향 엔티티를 참조하는 외래 키
7.4.2 일대다 조인 테이블
- 조인 테이블 컬럼 중 다(N)와 관련된 컬럼인 CHILD_ID에 유니크 제약조건을 지정해야 한다.
@Entity
@Getter @Setter
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
@Getter @Setter
public class Child {
@Id @GeneratedValue
@Column(name = "child_id")
private Long id;
private String name;
}
7.4.3 다대일 조인 테이블
- 조인 테이블 모양은 일대다와 같다.
@Entity
@Getter @Setter
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
@Getter @Setter
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;
}
7.4.4 다대다 조인 테이블
- 다대다 관계를 만들려면 조인 테이블의 두 컬럼을 합해서 하나의 복합 유니크 제약조건을 지정해야 한다.
- 즉 PARENT_ID와 CHILD_ID가 복합 유니크 제약조건으로 지정되야 한다.
@Entity
@Getter @Setter
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
@Getter @Setter
public class Child {
@Id @GeneratedValue
@Column(name = "child_id")
private Long id;
private String name;
}
7.5 엔티티 하나에 여러 테이블 매핑
- @SecondaryTable 어노테이션을 사용하면 한 엔티티에 여러 테이블을 매핑할 수 있다.
@Entity
@Table(name = "board")
@SecondaryTable(name = "board_detail", pkJoinColumns = @PrimaryKeyJoinColumn(name = "board_detail_id"))
public class Board {
@Id @GeneratedValue
@Column(name = "board_id")
private Long id;
private String title;
@Column(table = "board_detail")
private String content;
}
- @Table을 사용해서 Board 엔티티를 BOARD 테이블로 매핑한다.
- @SecondaryTable을 사용해서 BOARD_DETAIL 테이블을 매핑한다.
- @SecondaryTable.name: 매핑할 다른 테이블의 이름
- @SecondaryTable.pkJoinColumns: 매핑할 다른 테이블의 기본 키 컬럼 속성
- content 필드는 @Colmun을 사용해서 BOARD_DETAIL 테이블에만 매핑된다.
- title 필드처럼 테이블을 지정하지 않으면 기본 테이블인 BOARD에만 매핑된다.
- 더 많은 테이블을 매핑하려면 @SecondaryTables를 사용한다.
7.6 추가사항
- hashCode() 메소드는 언제 사용하는지?
- 데이터베이스 슈퍼타입, 서브타입이 무엇인지?
- 외래 키 참조 무결성이란?
- 데이터베이스 정규화
Leave a comment