고급 매핑

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() 메소드는 언제 사용하는지?
  • 데이터베이스 슈퍼타입, 서브타입이 무엇인지?
  • 외래 키 참조 무결성이란?
  • 데이터베이스 정규화

Tags:

Categories:

Updated:

Leave a comment