✅ 상속관계 매핑
객체지향에는 클래스끼리 상속관계가 존재하지만 데이터베이스에서 테이블 간의 상속관계는 지원하지 않는다. 그 대신 데이터베이스의 슈퍼타입, 서브타입 관계라는 모델링 기법을 통해서 객체의 상속관계를 표현할 수 있는 방법이 있다. Item 테이블이 슈퍼타입, Album, Movie, Book 테이블들이 서브타입으로 모델링된 것이다.
🔹조인 전략
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE") // default : DTYPE
@DiscriminatorValue("value")
부모 타입과 자식 타입을 모두 테이블로 만드는 전략이다. 이후 자식 타입 조회 시 조인을 통해 조회하는 방식으로 사용한다. 주의해야할 점은 객체는 타입으로 구분할 수 있지만, 테이블은 타입이 없다. 따라서 객체의 타입과 테이블을 매핑시켜주기 위해 DTYPE이라는 이름의 컬럼 하나를 추가하고 객체마다 타입을 구분하는 값을 넣어야 한다. 값을 넣지 않으면 default로 클래스 이름이 값으로 저장된다.
- Item(parent)
@Entity @Inheritance(strategy = InheritanceType.JOINED) @DiscriminatorColumn //기본값이 DTYPE public abstract class Item { @Id @GeneratedValue @Column(name = "ITEM_ID") private Long id; private String name; private int price; }
- Album(child)
@Entity @DiscriminatorValue("A") public class Album extends Item{ private String artist; }
속성값을 정의하지 않을 경우(”A”를 제거) 클래스명(Album)이 DTYPE 컬럼에 저장된다.
- Movie(child)
@Entity //@DiscriminatorValue("M") public class Movie extends Item { private String director; private String actor; }
- Book(child)
@Entity //@DiscriminatorValue("B") @PrimaryKeyJoinColumn(name = "BOOK_ID") public class Book extends Item { private String author; private String isbn;
hibernate 등의 몇몇 구현체는 구분 컬럼(@DiscriminatorValue)가 없이도 동작한다. 구분 컬럼이 없으면 구분 컬럼의 값은 Default로 클래스 이름이 저장된다.
- Main
public class Main { public static void main(String[] args) { //엔티티 매니저 팩토리 생성 EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook"); EntityManager em = emf.createEntityManager(); //엔티티 매니저 생성 EntityTransaction tx = em.getTransaction(); //트랜잭션 기능 획득 try { tx.begin(); //트랜잭션 시작 //TODO 비즈니스 로직 saveBook(em); tx.commit();//트랜잭션 커밋 } catch (Exception e) { e.printStackTrace(); tx.rollback(); //트랜잭션 롤백 } finally { em.close(); //엔티티 매니저 종료 } emf.close(); //엔티티 매니저 팩토리 종료 } public static void saveBook(EntityManager em) { Book book = new Book(); book.setAuthor("jh"); book.setIsbn("1234"); em.persist(book); Album album = new Album(); album.setArtist("new"); album.setEtc("known"); em.persist(album); Movie movie = new Movie(); movie.setActor("unknown"); movie.setDirector("sad"); em.persist(movie); em.flush(); em.clear(); //== find ==// Book findBook = em.find(Book.class, book.getId()); System.out.println("Book Author: " + findBook.getAuthor()); System.out.println("Book ISBN: " + findBook.getIsbn()); } }
- 장점
- 테이블이 정규화된다
- 외래 키 참조 무결성 제약 조건을 활용할 수 있다
- 저장 공간을 효율적으로 사용한다 ( 단일 테이블 전략과 비교해서 )
- 단점
- 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다
- 조회 쿼리가 복잡하다
- 데이터를 등록할 INSERT SQL을 두 번 실행한
- 장점
🔹단일 테이블 전략
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
@DiscriminatorValue("value")
테이블을 단 하나만 만든 후, 해당 테이블에 부모 속성, 자식 속성을 모두 포함시켜 사용한다. 조인 전략과 마찬가지로 서브타입을 구별하기위해 구분자 컬럼이 필수다. 주의해야할 점은 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다. 예를 들어 Book 엔티티를 저장하면 ITEM 테이블의 AUTHOR, ISBN 컬럼만 사용하고 다른 엔티티와 매핑된 ARTIST, DIRECTOR, ACTOR 컬럼은 사용하지 않으므로 null이 입력되기 때문이다. 그래서 조인 전략에 비해 저장 공간이 낭비된다.
- Item
@Entity @Getter @Setter @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn public abstract class Item { @Id @GeneratedValue @Column(name = "ITEM_ID") private Long id; private String name; private int price; }
👉 @Inheritance(strategy = InheritanceType.SINGLE_TABLE) 이 부분만 변경되고 나머지 코드는 동일하다.
- Main
👉 하나의 테이블에 모든 데이터가 저장된다.
- 장점
- 조인이 필요 없기 때문에 일반적인 상황에서 조회 성능이 빠르다.
- 조인이 필요 없기 때문에 조회 쿼리가 단순하다.
- 단점
- 자식 엔티티에 매핑한 컬럼은 모두 null을 허용해야한다.
- 테이블의 컬럼 길이가 커지기 때문에 상황에 따라 조회 성능이 오히려 느려질 수 있다.
🔹구현 클래스마다 테이블 전략
부모 테이블을 만들지 않고, 각각의 자식 테이블의 부모 타입의 속성을 갖게 만드는 방법이다.
👉 실무에서 사용하기 어렵다. 일반적으로 추천하지 않는 전략이다.
✅ 공통 속성 -@MappedSuperclass
@MappedSuperclass은 부모 클래스는 테이블과 매핑하지 않고 자식 클래스에게 매핑 정보만 제공하는 클래스를 만드는 어노테이션이다. Member 클래스와 Seller 클래스는 공통 속성 id와 name을 상위 클래스에 정의해 속성을 물려 받게 할 수 있다. 객체간의 관계를 부모-자식 관계로 설정하기 위해 extends(상속)으로 표현했지만, 종속적인 관계가 아니고 단순히 공통 속성들만 재사용하기 위해 상속을 사용하는 것이다. 실제로 BaseEntity는 DB에 따로 테이블로 저장되지 않으며 Member과 Seller 각각의 테이블에 BaseEntity의 속성이 추가되는 것을 볼 수 있다. 상위 클래스는 엔티티가 아니고 자식 클래스에 속성만 물려준다. 엔티티가 아니기 때문에 당연히 상위 클래스로 조회, 검색 할 수 없다.
id, name이라는 같은 이름의 컬럼이 존재하지만 개념적으로 두 테이블은 서로 관련없는 독립적인 테이블이다. BaseEntity로부터 단지 공통된 속성만 물려받았다.
@MappedSuperclass
public abstract class BaseEntity { // 객체로 만들어 사용할 일이 없기 때문에 추상클래스로 선언한다
private LocalDate createdDate;
private LocalDate lastModifiedDate;
//.. getter, setter ..//
}
실무에서는 주로 모든 엔티티에서 등록일, 수정일, 등록자, 수정자를 추적한다. 공통 속성들을 모은 BaseEntity 클래스를 만들었다. 실제 객체화 할 일이 없기 때문에 추상 클래스로 만들었다. 상위 클래스는 @MappedSuperclass
를 통해 단순히 속성 정보만 모은 클래스임을 명시해줘야한다.
- Order
@Entity @Table(name = "ORDERS") public class Order extends BaseEntity { @Id @GeneratedValue @Column(name = "ORDER_ID") private Long id; @ManyToOne @JoinColumn(name = "MEMBER_ID") private Member member; //주문 회원
- Member
@Entity public class Member extends BaseEntity { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String name; private String city; private String street; private String zipcode; @OneToMany(mappedBy = "member") private List<Order> orders = new ArrayList<Order>();
👉 Order, Member에서 정의하지 않고, BaseEntity에서 정의한 속성인 createdDate와 lastModifiedDate가 각각 필드로 저장된 것을 확인할 수 있다.
📌특징
- 테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용한다
- @MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find(), JPQL을 사용할 수 없다
- 객체로 생성해서 사용할 일은 거의 없기 때문에 추상 클래스로 만들기를 권장한다
Uploaded by N2T