데이터베이스 테이블의 연관관계를 설계하는 방법은 크게 2가지 입니다.
- 조인 컬럼 사용(외래 키)
- 조인 테이블 사용(테이블 사용)
테이블 간에 관계는 주로 조인 컬럼이라 부르는 외래 키 컬럼을 사용해서 관리합니다.
위의 그림에서 회원과 사물함이 있고 각각 테이블에 데이터를 등록했다가 회원이 원할 때 사물함을 선택할 수 있다고 가정해보겠습니다. 회원이 사물함을 사용하기 전까지 아직 둘 사이에 관계가 없으므로 MEMBER 테이블의 LOCKER_ID 외래 키에 null을 입력해두어야 합니다. 이렇게 외래 키에 null을 허용하는 관계를 선택적 비식별 관계라고 합니다.
선택적 비식별 관계는 외래 키에 null을 허용하므로 회원과 사물함을 조인할 때 외부 조인을 사용해야 합니다. 실수로 내부 조인을 사용하면 사물함과 관계 없는 회원은 조회되지 않습니다. 그리고 회원과 사물함이 아주 가끔 관계를 맺는다면 외래 키 값 대부분이 null로 지정되는 단점이 있습니다.
아래 그림은 조인 컬럼을 사용하는 대신에 조인 테이블을 사용해서 연관관계를 관리하고 있습니다.
이 방법은 조인 테이블이라는 별도의 테이블을 사용해서 연관관계를 관리합니다.
조인 컬럼을 사용하는 방법은 단순히 외래 키 컬럼만 추가해서 연관관계를 맺지만 조인 테이블을 사용하는 방법은 연관관계를 관리하는 조인 테이블을 추가하고 여기서 두 테이블의 외래 키를 가지고 연관관계를 관리합니다. 따라서 MEMBER, LOCKER 테이블에는 연관관계를 관리하기 위한 외래 키 컬럼이 없습니다.
조인 테이블의 가장 큰 단점은 테이블을 하나 추가해야 한다는 점입니다. 따라서 관리해야 하는 테이블이 늘어나고 회원과 사물함 두 테이블을 조인하려면 MEMBER_LOCKER 테이블까지 추가로 조인해야 합니다. 따라서 기본은 조인 컬럼을 사용하고 필요하다고 판단되면 조인 테이블을 사용하면 됩니다.
조인 테이블 일대일에서 조인 테이블을 살펴보겠습니다. 일대일 관계를 만들려면 조인 테이블의 외래 키 컬럼 각각에 총 2개의 유니크 제약조건을 걸어야 합니다.
@Setter
@Getter
@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;
}
@Getter
@Setter
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
}
@Test
@Transactional
@Rollback(false)
public void 식별자_테스트() throws Exception {
Parent parent = new Parent();
parent.setName("임종수");
Child child = new Child();
child.setName("임준영");
parent.setChild(child);
em.persist(parent);
em.persist(child);
}
@Setter
@Getter
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@OneToMany
private List<Child> child = new ArrayList<Child>();
}
@Getter
@Setter
@Entity
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;
}
다대다 관계를 만들려면 조인 테이블의 두 컬럼을 합해서 하나의 복합 유니크 제약조건을 걸어야 합니다.
@Setter
@Getter
@Entity
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<Child>();
}
@Getter
@Setter
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
}
잘 사용하지는 않지만 @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;
}
위의 코드를 살펴보면 Board 엔티티는 @Table을 사용해서 BOARD 테이블과 매핑했습니다. 그리고 @SecondaryTable을 사용해서 BOARD_DETAIL 테이블을 추가로 매핑했습니다.
- @SecondaryTable.name: 매핑할 다른 테이블의 이름, 예제에서는 테이블명을 BOARD_DETAIL로 지정했습니다.
- @SecondaryTable.pkJoinColumn: 매핑할 다른 테이블의 기본 키 컬럼 속성, 예제에서는 기본 키 컬럼명을 BOARD_DETAIL_ID로 지정했습니다.
@Column(table = "BOARD_DETAIL")
private String content;
content 필드는 @Column(table = "BOARD_DETAIL")을 사용해서 BOARD_DETAIL 테이블의 컬럼에 매핑했습니다. title 필드처럼 테이블을 지정하지 않으면 기본 테이블인 BOARD에 매핑됩니다.
@SecondaryTables({
@SecondaryTable(name = "BOARD_DETAIL"),
@SecondaryTable(name = "BOARD_FILE")
})
더 많은 테이블을 매핑하려면 @SecondaryTables를 사용하면 됩니다.
하지만 이 방법은 항상 두 테이블을 조회하므로 최적화하기가 어렵습니다. 반면에 일대일 매핑은 원하는 부분만 조회 할 수 있고 필요하면 둘다 함께 조회됩니다.
참조 문헌: JPA ORM 표준 프로그래밍