JPA 가 제공하는 고급매핑에 대해 알아보자.
순서
@MappedSuperclass
데이터베이스에는 상속이라는 개념이 없다.
대신 슈퍼타입 서브타입 관계라는 모델링 기법이 그나마 객체의 상속과 비슷하다.
그렇다면 이러한 모델링을 객체 상속관계로 매핑하는 3가지 방법에 대해 알아보자.
엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본키를 받아서 기본 키+ 외래 키로 사용하는 전략이다.
객체에는 타입을 통해서 구분이 가능하지만, 테이블은 타입을 구분하는 컬럼(= DTYPE)을 사용해서 서로 구분한다.
예제 테이블 그림
엔티티 코드
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "emp_type")
public abstract class Employee {
@Id @GeneratedValue
@Column(name = "emp_id")
private Long id;
private String empName;
private String address;
}
@Entity
@Setter
@Getter
@DiscriminatorValue("hour")
public class HourlyEmployee extends Employee {
private String hourlyRate;
}
@Entity
@Getter
@Setter
@DiscriminatorValue("consult")
public class Consultant extends Employee {
private String payRate;
}
@Entity
@Setter
@Getter
@DiscriminatorValue("salary")
public class SalariedEmployee extends Employee {
private String annualSalary;
}
상세 설명
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "emp_type")
@DiscriminatorValue("salary")
구분 컬럼을 사용하되 각 테이블 별로 있던 컬럼을 그냥 한 테이블에 다 때려 박는다.
예제 테이블
엔티티 코드
위의 조인 전략 코드에서 부모 클래스인 Employee 의 애노테이션만 수정하면 된다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 얘만 변경
@DiscriminatorColumn(name = "emp_type")
public abstract class Employee {
@Id @GeneratedValue
@Column(name = "emp_id")
private Long id;
private String empName;
private String address;
}
생성된 테이블 결과
이 방법은 데이터베이스 설계자와 ORM 전문가 모두가 추천하지 않으니, 될 수 있으면 사용하지 말자.
엔티티 코드
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) // 요것만 수정
@DiscriminatorColumn(name = "emp_type")
public abstract class Employee {...}
생성된 테이블
이전에 본 전략들은 모두 데이터베이스 테이블과 매핑이 됐는데,
이번엔 부모 클래스에 테이블이 매핑되지 않고, 오직 자식 클래스에게 매핑 정보만 넘겨주겠다.
@MappedSuperclass
가 있으면 테이블 매핑이 일어나지 않는다.
이전에 사용했던 코드를 그대로 쓰겠다.
다만 이전에 Employee 추상 클래스 위에 있는 애노테이션들을 아래처럼 수정하겠다.
공통 매핑 정보 추상 클래스
@MappedSuperclass // @Entity 가 사라지고 이게 대신 쓰인다.
@Getter @Setter
public abstract class Employee {
@Id @GeneratedValue
@Column(name = "emp_id")
private Long id;
private String empName;
private String address;
}
이렇게하면 끝이다!
이러고 자식 클래스에서 extend를 하기만 하면 모든 매핑 정보를 다 물려받는다.
참고로 자식 클래스에서 부모로부터 받은 매핑 정보를 재정의하려면 아래처럼 한다.
엔티티 코드
@Entity
@Setter
@Getter
@AttributeOverride(name = "id", column = @Column(name = "employee_id"))
public class HourlyEmployee extends Employee {
private String hourlyRate;
}
테이블 생성 결과
여러개의 매핑 정보를 수정하고 싶다면 아래처럼 하자.
@Entity
@Setter
@Getter
@AttributeOverrides({
@AttributeOverride(name = "id", column = @Column(name = "employee_id")),
@AttributeOverride(name = "empName", column = @Column(name = "employee_name"))
})
public class HourlyEmployee extends Employee {
private String hourlyRate;
}
테이블 생성 결과
hourly_employee
테이블은 AttributeOverrides
가 잘 적용된 것을 확인할 수 있다. 참고:
@MappedSuperclass
와table_per_class
의 차이점
이 글을 참고하자. 나는Vlad Mihalcea
의 답변이 난 이해하기 편했다.
참고2:
엔티티(@Entity
)는 엔티티(@Entity
)이거나@MappedSuperclass
로 지정한 클래스만 상속 가능
DB Table 에서 외래키가 기본키에 포함 되면 식별관계 아니면 비식별 관계이다.
아래 그림을 통해서 차이점을 보자.
식별 관계 그림
비식별 관계 그림
정말 단순하게 생각해서 복합키는 @Id
를 2개 쓰면 되지 않을까 싶다.
한번 아래 엔티티 코드 작성후 테스트를 돌려보자.
엔티티 코드
@Entity
public class TestEntity {
@Id
private String name;
@Id
private String phoneNumber;
}
콘솔 출력
😱
에러가 난다. JPA에서는 이런 방식으로 복합 키를 지원하지 않는다.
JPA에서는 @IdClass
와 EmbeddedId
2가지 애노테이션을 제공하며 우리는 이를 사용해서 식별자 클래스를 작성하면 된다.
잊었을까봐 말하지만 JPA의 영속성 컨텍스트는 엔티티를 보관할 때 식별자를 사용한다.
그런데 JPA 에서는 이 식별자를 실제 비교하는 방법은 식별자 객체의 equals 와 hashcode 메소드를 통해서 이루어진다. 그러므로 식별자 클래스에서 이 2개의 메소드는 필수적으로 구현해야 한다..
복합키로 구성시에 복합키 구성원 중 그 어떤 필드도 @GeneratedValue
를 사용하지 못한다.
이런 상황에서 어떻게 매핑을 하는지 코드를 보자.
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode // 반드시 equals 와 hashcode 는 구현해야 한다!
public class MainId implements Serializable {
private String id1; //MainEntity.id1 매핑
private String id2; //MainEntity.id2 매핑
}
@Entity
@Table(name = "Main")
@IdClass(MainId.class)
public class MainEntity {
@Id
@Column(name = "main_id1")
private String id1; // MainId.id1과 연결
@Id
@Column(name = "main_id2")
private String id2; // MainId.id1과 연결
private String name;
}
참고. 만약 id1, id2 라는 필드 명을 안 쓰면?
아래와 같은 예외가 터진다.
Property of @IdClass not found in entity me.dailycode.main.domain._05.MainClass: id1
식별자 클래스 작성법
식별자 클래스의 속성명
== 엔티티가 사용하는 식별자의 속성명
Serializable
구현 필수equals
, hashcode
구현 필수public class
@Entity
public class SubEntity {
@Id
@Column(name = "sub_id")
private String id;
@ManyToOne
@JoinColumns({
@JoinColumn(name = "main_id1", referencedColumnName = "main_id1"),
@JoinColumn(name = "main_id2", referencedColumnName = "main_id2")
})
private MainEntity main;
}
- 엔티티 저장 코드
MainEntity main = new MainEntity();
// 아래 2줄 처럼 하면 JPA가 알아서 MainId 객체 생성 후, 영속성 컨텍스트의 키로 사용
main.setId1("mainId1");
main.setId2("mainId2");
main.setName("mainEntity");
em.persist(main);
- 엔티티 조회 코드
MainId mainId = new MainId("mainId1", "mainId2");
MainEntity mainEntity = em.find(MainEntity.class, mainId);
- 나가는 쿼리
Hibernate:
select
mainentity0_.main_id1 as main_id1_7_0_,
mainentity0_.main_id2 as main_id2_7_0_,
mainentity0_.name as name3_7_0_
from
main mainentity0_
where
mainentity0_.main_id1=?
and mainentity0_.main_id2=?
@IdClass
가 데이터베이스에 맞춘 방법이라면, @EmbededId
는 조금 더 객체지향적 방식이다.
코드를 보자.
@Entity
@Table(name = "Main")
@Setter @ToString
public class MainEntity {
@EmbeddedId
private MainEntityId id;
private String name;
}
@Embeddable
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode // 반드시 구현해야함
public class MainEntityId implements Serializable {
@Column(name = "main_id1")
private String id1;
@Column(name = "main_id2")
private String id2;
}
식별자 클래스 작성법
@Embeddable
을 붙여준다.Serializable
을 구현한다.equals
, hashcode
메소드를 구현한다.@IdClass
을 사용하면 기본키 매핑(=@Id
)을 엔티티 클래스에 직접 작성했지만,
@EmbeddedId
을 사용하면 기본키 매핑이 @Id
대신 @EmbeddedId
를 통해서 이루어지며
그 식별자 클래스는 @Embeddable
애노테이션을 사용한다.
등록 및 조회 테스트
MainEntity main = new MainEntity();
MainEntityId mainEntityId = new MainEntityId("mainId1", "mainId2");
main.setId(mainEntityId);
main.setName("mainEntity");
em.persist(main);
em.flush();
em.clear();
MainEntity mainEntity = em.find(MainEntity.class, mainEntityId);
System.out.println("mainEntity = " + mainEntity);
// mainEntity = MainEntity(id=MainEntityId(id1=mainId1, id2=mainId2), name=mainEntity)
@IdClass vs @EmbeddedId
그냥 본인 취향에 맞는 걸 쓰자 ^^;
참고 (중요!)
복합 키에는@GeneratedValue
를 사용하지 못함.
이 구조는 대체 어떻게 매핑할까.
@Entity @Table(name = "Super")
public class SuperEntity {
@Id
@Column(name = "super_id")
private String id;
private String name;
}
@EqualsAndHashCode
public class MiddleId implements Serializable {
private String super_;
private String middleId;
}
@Entity
@Table(name = "Middle")
@IdClass(MiddleId.class)
public class MiddleEntity {
@Id // PK
@ManyToOne// FK
@JoinColumn(name = "super_id")
private SuperEntity super_;
// PK
@Id
private String middleId;
private String name;
}
@EqualsAndHashCode
public class DerivedId implements Serializable {
private MiddleId middle; // DerivedEntity.middle 매핑
private String derivedId; // DerivedEntity.derivedId 매핑
}
@Entity @Table(name = "Derived")
@IdClass(DerivedId.class)
public class DerivedEntity {
@Id
@ManyToOne
@JoinColumns({
@JoinColumn(name = "super_id"),
@JoinColumn(name = "middle_id")
})
private MiddleEntity middle;
@Id
private String derivedId;
private String name;
}
어렵다😱!
@Embedded
로 식별 관계를 구성할 때는 @MapsId
를 사용해야 한다.
@Entity @Table(name = "Super")
public class SuperEntity {
@Id
@Column(name = "super_id")
private String id;
private String name;
}
@Entity
@Table(name = "Middle")
public class MiddleEntity {
@EmbeddedId
private MiddleId id;
@MapsId("superId")
@ManyToOne
@JoinColumn(name = "super_id")
public SuperEntity super_;
private String name;
}
@Embeddable
@EqualsAndHashCode
public class MiddleId implements Serializable {
private String superId; // MapsId("superId")로 매핑
@Column(name = "middle_id")
private String id; // 필드명을 MiddleEntity 의 PK와 매핑
}
@Entity @Table(name = "Derived")
public class DerivedEntity {
@EmbeddedId
private DerivedId id;
@MapsId("middleId")
@ManyToOne
@JoinColumns({
@JoinColumn(name = "super_id"),
@JoinColumn(name = "middle_id")
})
private MiddleEntity middle;
private String name;
}
@Embeddable
@EqualsAndHashCode
public class DerivedId implements Serializable {
@Embedded
private MiddleId middleId;
private String derivedId;
public MiddleId getMiddleId() {
return middleId;
}
}
테이블 생성 모습
결론: 어렵다😱!
그래도 쓰다보면 익숙해질지도?
@Entity
public class SuperEntity {
@Id
@Column(name = "super_id")
private String id;
private String name;
}
@Entity
public class MiddleEntity {
@Id
private String middleId;
@ManyToOne
@JoinColumn(name = "super_id")
private SuperEntity superEntity;
private String name;
}
@Entity
public class DerivedEntity {
@Id
private String derivedId;
@ManyToOne
@JoinColumn(name = "middle_id")
private MiddleEntity middleEntity;
private String name;
}
이전에 비해서 너무나도 쉽다. 게다가 복합키도 없어서 더더욱 그렇다.
@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;
}
테이블이 서로 연관관계를 맺는 방법은 2가지다.
조인 컬럼 그림
조인 테이블 그림
참고:
책에서는 1:1, 1:N, N:1, N:M 순으로 조인테이블 매핑 방식을 알려주지만...
내가 보기에는 1:N 과 N:1 사실상 똑같고, N:M 은 이전에 했던 @JoinTable 내용의 반복이다.
그러니 1:1, 1:N 조인 테이블만 가볍게 보고 넘어가겠다.
엔티티 코드
@Entity
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
public class Child {
@Id @GeneratedValue
@Column(name = "child_id")
private Long id;
private String name;
}
참고: 1:N 에서 N 쪽에 있는 PK 를 조인테이블이 참조하게 되는데, 이 값은 UNIQUE 제약 조건을 줘야한다.
@Entity
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
public class Child {
@Id @GeneratedValue
@Column(name = "child_id")
private Long id;
private String name;
}