본문 바로가기
Framework and Tool/JPA

JPA - 고급맵핑 - 조인 테이블

by ocwokocw 2021. 7. 12.

- 참조: 자바 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 엔티티를 참조하고 있다.

댓글