- 참조: 자바 ORM 표준 JPA 프로그래밍
- 조인 테이블
DB 의 테이블 연관관계를 설계하는 방법에는 컬럼을 이용한 조인과 조인 테이블을 사용하는법 2 가지가 존재한다.
위 다이어그램에서 MEMBER 테이블에서 LOCKER 와 연관관계를 맺을 때 LOCKER_ID 조인 컬럼을 이용한다. 만약 회원이 5 명 있을 때 2 명에게만 사물함이 할당되었고 나머지 회원에게는 사물함이 아직 할당되지 않았다면 해당 회원의 LOCKER_ID 컬럼에는 NULL 이 할당되어 있을것이다. FK 에 NULL 이 있다면 INNER JOIN 사용 시 MEMBER 를 인식할 수 없으므로 MEMBER 를 기준으로 사물함 존재유무를 나타내야 한다면 OUTER JOIN 을 이용해야 한다.
위 다이어그램은 MEMBER_LOCKER 조인 테이블을 사용하여 MEMBER 와 LOCKER 가 연관을 맺는다. 외래키 관리는 조인테이블에서 하므로 MEMBER 와 LOCKER 는 관계를 맺기 위해 FK 를 두지 않아도 된다. 하지만 관리해야하는 테이블이 하나 추가된다는 단점이 있고 조회시에도 조인 테이블을 추가로 조인해야 한다. 보통 관계 테이블은 다대다 관계를 일대다, 다대일로 풀어내기 위해 사용한다.
- 1 : 1 조인 테이블
1 : 1 연관에서 조인테이블 방식을 사용하려면 조인 테이블에 2 개의 유니크 제약조건을 걸어야 한다. (PARENT_ID 를 PK 로 설정했다면 이미 유니크 제약조건이 걸려있으며 CHILD_ID 에는 유니크 제약조건을 별도로 사용한다.)
@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", referencedColumnName = "PARENT_ID")},
inverseJoinColumns = {@JoinColumn(name = "CHILD_ID", referencedColumnName = "CHILD_ID")})
private Child child;
..............
@Entity
public class Child{
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "child")
private Parent parent;
Parent 엔티티를 살펴보자. 다중성은 1 : 1 이므로 @OneToOne 을 사용하였다. 연관관계 맵핑은 @JoinTable 어노테이션을 사용하였는데 앞에서 살펴본적이 있지만 다시 한번 속성의 의미를 알아보자.
- name: 사용할 조인테이블의 이름을 설정한다.
- joinColumns: 현재 엔티티에서 참조할 FK 를 설정한다.
- inverseJoinColumns: 상대방 엔티티에서 참조할 FK 를 설정한다.
Child 에서 @OneToOne 에 mappedBy 를 설정한것은 Child 에서 Parent 를 참조하는 양방향일 경우에 작성한다. 만약 단방향이라면 해당 코드는 없어도 무방하다.
public static void save(EntityManager em) {
EntityTransaction tx = em.getTransaction();
tx.begin();
Parent parent = new Parent();
parent.setName("PARENT_NAME#1");
em.persist(parent);
Child child = new Child();
child.setName("CHILD_NAME#1");
em.persist(child);
parent.setChild(child);
tx.commit();
em.close();
}
위와 같이 테스트 코드를 작성하고 H2 Console 에서 조회하면 아래와 같이 결과가 나온다.
- 1 : N 조인테이블
위 다이어그램은 1 : N 의 관계에서 조인테이블 사용을 나타낸다. PARENT : PARENT_CHILD 는 OneToMany 형태로 맵핑된다. 원래 PARENT : CHILD 가 1 : N 으로 맵핑되는데 CHILD 대신 PARENT_CHILD 관계테이블이 맵핑 역할을 대신하기 때문이다. PARENT_CHILD : CHILD 는 OneToOne 으로 맵핑된다.
PARENT_CHILD 에서 CHILD_ID 에만 PK 를 걸어도 고유하게 구분할 수 있기 때문에 PARENT_ID 는 자연스럽게 FK 가 된다.
@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", referencedColumnName = "PARENT_ID")},
inverseJoinColumns = {@JoinColumn(name = "CHILD_ID", referencedColumnName = "CHILD_ID")})
private List<Child> child = new ArrayList<>();
...........
public void addChild(Child child) {
this.child.add(child);
}
만약 조인테이블이 아니라 조인컬럼으로 맵핑한다면 1 : N 의 관계에서 N 쪽에 @JoinColumn 으로 외래키를 설정했겠지만 조인 테이블이므로 1 쪽에 @OneToMany 어노테이션과 함께 @JoinTable 을 달면 된다. Child 엔티티는 아래처럼 따로 작성해줄 코드는 없다.
@Entity
public class Child{
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
.........
저장하는 테스트코드를 작성해보자.
public static void save(EntityManager em) {
EntityTransaction tx = em.getTransaction();
tx.begin();
Parent parent = new Parent();
parent.setName("PARENT_NAME#1");
em.persist(parent);
Child child1 = new Child();
child1.setName("CHILD_NAME#1");
em.persist(child1);
Child child2 = new Child();
child2.setName("CHILD_NAME#2");
em.persist(child2);
parent.addChild(child1);
parent.addChild(child2);
tx.commit();
em.close();
}
H2 Console 로 조회해보면 데이터도 잘 들어간다.
- N : 1 조인테이블
N : 1 은 1 : N 과 방향만 반대이므로 테이블설계는 같다. @JoinTable 어노테이션만 반대 엔티티에서 맵핑해주면 된다.
@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", referencedColumnName = "CHILD_ID")},
inverseJoinColumns = {@JoinColumn(name = "PARENT_ID", referencedColumnName = "PARENT_ID")})
private Parent parent;
............
@Entity
public class Parent{
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "parent")
private List<Child> child = new ArrayList<>();
Child 엔티티에서 @JoinTable 을 사용하고 joinColumns 와 inverseJoinColumns 의 컬럼맵핑을 1 : N 과 반대로 해준다. 위에서 @ManyToOne 의 optional 이 false 인데, 이 옵션이 false 인 경우 Parent 가 없는 Child 는 없다는것을 보장해주기 때문에 검색시 outer join 대신 inner join 을 사용할 수 있다.
public static void save(EntityManager em) {
EntityTransaction tx = em.getTransaction();
tx.begin();
Parent parent = new Parent();
parent.setName("PARENT_NAME#1");
em.persist(parent);
Child child1 = new Child();
child1.setName("CHILD_NAME#1");
child1.setParent(parent);
em.persist(child1);
Child child2 = new Child();
child2.setName("CHILD_NAME#2");
child2.setParent(parent);
em.persist(child2);
tx.commit();
em.close();
}
때문에 Child 엔티티를 영속화시킬 때 parent 를 설정하고 영속화시켜야 한다. 결과는 1 : N 과 같다.
- M : N 조인 테이블
M : N 도 크게 다르지는 않다. 다만 관계테이블이 복합 키가 되어야 한다. 하나의 컬럼에만 UNIQUE 조건을 걸 경우 M : N 으로 맵핑할 수 없기 때문이다.
@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", referencedColumnName = "PARENT_ID")},
inverseJoinColumns = {@JoinColumn(name = "CHILD_ID", referencedColumnName = "CHILD_ID")})
private List<Child> child = new ArrayList<>();
...........
@Entity
public class Child{
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
테스트 코드는 아래와 같다.
public static void save(EntityManager em) {
EntityTransaction tx = em.getTransaction();
tx.begin();
Parent parent = new Parent();
parent.setName("PARENT_NAME#1");
em.persist(parent);
Parent parent2 = new Parent();
parent2.setName("PARENT_NAME#2");
em.persist(parent2);
Child child1 = new Child();
child1.setName("CHILD_NAME#1");
em.persist(child1);
Child child2 = new Child();
child2.setName("CHILD_NAME#2");
em.persist(child2);
parent.getChild().add(child1);
parent.getChild().add(child2);
parent2.getChild().add(child1);
parent2.getChild().add(child2);
tx.commit();
em.close();
}
H2 Console 에서 조회하니 2 개의 Parent 엔티티가 아래처럼 2 개의 Child 엔티티를 참조하고 있다.
'Framework and Tool > JPA' 카테고리의 다른 글
JPA - 고급맵핑 - 요구사항 분석과 맵핑3 (0) | 2021.07.13 |
---|---|
JPA - 고급맵핑 - Multi 테이블 맵핑 (0) | 2021.07.12 |
JPA - 고급맵핑 - 복합키와 식별 관계 맵핑 (2) | 2021.07.11 |
JPA - 고급맵핑 - MappedSuperclass (0) | 2021.07.10 |
JPA - 고급맵핑 - 슈퍼타입과 서브타입 (0) | 2021.07.10 |
댓글