BOOK 3 - 자바 ORM 표준 JPA 프로그래밍(7)

고급 매핑

  • 상속 관계 매핑
    • 객체의 상속 관계를 데이터베이스에 어떻게 매핑하는지 다룬다.
  • @MappedSuperClass
    • 등록일, 수정일 같이 여러 entity에서 공통으로 사용하는 매핑 정보만 상속받고 싶을 때 사용한다.
  • 복합 키와 식별 관계 매핑
    • 데이터베이스의 식별자가 하나 이상일 때 매핑하는 방법을 다룬다.
    • 데이터베이스 설계에서 이야기 하는 식별관계와 비식별 관계에 대해 다룬다.
  • 조인 테이블
    • 연관관계를 관리하는 연결 테이블을 두어 연관관계를 맺을 수 있다.
    • 이 조인 테이블을 매핑하는 방법을 다룬다.
  • 엔티티 하나에 여러 테이블 매핑하기
    • 보통 엔티티 하나에 테이블 하나를 매핑하지만 엔티티 하나에 여러 테이블을 매핑하는 방법도 있다.

상속 관계 매핑

  • 관계형 데이터베이스에는 상속이라는 개념이 없다.
    • 대신 Super-Type Sub-Type Relationship 이라는 모델링 기법이 객체의 상속 개념과 가장 유사하다.
  • ORM에서 이야기하는 상속 관계 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것이다.

image

  • 슈퍼타입 서브타입 논리 모델을 실제 물리 모델인 테이블로 구현할 때는 다음과 같은 3가지 방법을 선택할 수 있다.
    • 각각의 테이블로 변환 : 각각을 모두 테이블로 만들고 조회할 때 조인을 사용한다. JPA에서는 조인 전략이라고 한다.
    • 통합 테이블로 변환 : 테이블을 하나만 사용해서 통합한다. JPA에서는 단일 테이블 전략이라고 한다.
    • 서브타입 테이블로 변환 : 서브 타입마다 하나의 테이블을 만든다. JPA에서는 구현 클래스마다 테이블 전략이라고 한다.

조인 전략

  • 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략
    • 조회할 때 조인을 자주 사용한다.
    • 테이블에는 type의 개념이 없기 때문에 type을 구분하는 컬럼을 부모테이블에 추가해야 한다.
      • 여기서는 DTYPE 컬럼을 타입 구분 컬럼으로 사용한다.
@Entity
@Inheritance(strategy=InheritanceType.JOINED)
@DisCriminatorColumn(name="DTYPE")
public abstract class Item {
  @Id
  @GeneratedValue
  @Column(name="ITEM_ID")
  private Long id;
  
  private String name;
  private int price;
  ...
}

@Entity
@DiscriminatorValue("A")
public class Album extends Item {
  private String artist;
  ...
}

@Entity 
@DiscriminatorValue("M")
@PrimaryKeyJoinColumn(name="MOVIE_ID")
public class Movie extends Item {
  private String director;
  private String actor;
  ...
}
  • @Inheritance(strategy=InheritanceType.JOINED)
    • 상속 매핑은 부모 클래스에 @Inheritance 를 선언해줘야 한다.
    • strategy 속성으로 매핑 전략을 지정해준다.
  • @DiscriminatorColumn(name="DTYPE")
    • 부모 클래스에 타입 구분 컬럼을 지정할 때 사용한다.
    • 이 컬럼으로 저장된 자식 테이블을 구분할 수 있다.
    • 기본값이 DTYPE 이므로 이 경우 생략해서 @DiscriminatorColumn 으로 선언해도 된다.
  • @DiscriminatorValue("A")
    • entity를 저장할 때 구분 컬럼에 입력할 값을 지정한다.
  • @PrimaryKeyJoinColumn(name="MOVIE_ID")
    • 기본적으로 자식테이블은 부모 테이블의 ID 컬럼명을 그대로 사용한다.
    • 자식 테이블의 기본 키 컬럼명을 변경하고 싶을 때 사용한다.
    • name 속성으로 재정의할 컬럼명을 지정해준다.

조인 전략의 장점과 단점

  • 장점
    • 테이블이 정규화된다.
    • 외래 키 참조 무결성 제약조건을 활용할 수 있다.
    • 저장공간을 효율적으로 사용한다.
  • 단점
    • 조회 시에 조인이 많이 사용되어 성능이 저하될 수 있다.
    • 조회 쿼리가 복잡하다.
    • 데이터를 등록할 때 INSERT SQL이 두번 실행된다.

단일 테이블 전략

  • 테이블을 하나만 사용하고 구분 컬럼으로 어떤 자식 데이터가 저장되었는지 구분한다.
  • 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다.

image

  • 자식 엔티티에 매핑한 컬럼은 모두 null 을 허용해야 한다.
    • 예를 들어 Book 엔티티는 Movie 엔티티의 directorAlbum 엔티티의 artist 컬럼은 사용하지 않기 때문이다.
@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="DTYPE")
public abstract class Item {
  @Id
  @GeneratedValue
  @Column(name="ITEM_ID")
  private Long id;
  
  private String name;
  private int price;
  ...
}

@Entity
@DiscriminatorValue("A")
public class Album extends Item { ... }

@Entity
@DiscriminatorValue("M")
public class Movie extends Item { ... }

@Entity
@DiscriminatorValue("B")
public class Book extends Item { ... }
  • InheritanceType.SINGLE_TABLE 로 단일 테이블 전략을 사용함을 지정해준다.
  • 테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수로 사용해야 한다.
  • @DiscriminatorValue 를 지정하지 않으면 기본으로 엔티티 이름을 사용한다.
    • 예를 들어 Movie , Album , Book 으로 사용한다.

단일 테이블 전략의 장점과 단점

  • 장점
    • 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다.
    • 조회 쿼리가 단순하다.
  • 단점
    • 자식 엔티티가 매핑한 컬럼은 모두 null 을 허용해야 한다.
    • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.
      • 상황에 따라 조회 성능이 오히려 느려질 수 있다.

구현 클래스마다 테이블 전략

  • 자식 엔티티마다 테이블을 만들고 각 테이블에 필요한 컬럼이 모두 있다.

@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public class abstract Item {
  @Id
  @GeneratedValue
  @Column(name="ITEM_ID")
  private Long id;
  
  private String name;
  private int price;
  ...
}

@Entity
public class Album extends Item { ... }

@Entity
public class Movie extends Item { ... }

@Entity
public class Book extends Item { ... }
  • InheritanceType.TABLE_PER_CLASS 로 구현 클래스마다 테이블 전략을 사용함을 지정해준다.
  • 구현 클래스마다 테이블을 만들기 때문에 구분 컬럼을 사용하지 않는다.

구현 클래스마다 테이블 전략의 장점과 단점

  • 장점
    • 서브 타입을 구분해서 처리할 때 효과적이다.
    • not null 제약 조건을 사용할 수 있다.
  • 단점
    • 여러 자식 테이블을 함께 조회할 때 UNION 을 사용해야 하므로 성능이 느리다.
    • 자식 테이블을 통합해서 쿼리하기 어렵다.
  • 일반적으로 이 전략은 추천하지 않는 전략이다.

@MappedSuperclass

  • 부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고 싶을 때 사용한다.
  • @Entity 는 실제 테이블과 매핑되지만 @MappedSuperclass 는 실제 테이블과는 매핑되지 않는다.
    • 단순히 매핑 정보를 상속할 목적으로만 사용된다.

image

  • MemberSeller 는 서로 관계가 없는 테이블(엔티티)이다.
  • 테이블은 그대로 두고 객체 모델의 id , name 두 공통 속성을 부모 클래스로 모으고 객체 상속 관계로 만들 수 있다.
@MappedSuperclass
public abstract class BaseEntity {
  @Id
  @GeneratedValue
  private Long id;
  
  private String name;
}

@Entity
public class Member extends BaseEntity {
  private String email;
  ...
}

@Entity
public class Seller extends BaseEntity {
  private String shopName;
  ...
}
  • BaseEntity 에는 객체들이 주로 사용하는 공통 매핑 정보를 정의한다.

    • 자식 엔티티들은 상속을 통해 BaseEntity 의 매핑 정보를 물려받는다.
  • BaseEntity 는 실제 테이블과 매핑할 필요가 없고, 자식 엔티티에게 공통으로 사용되는 매핑 정보만 제공하면 된다.

  • 부모로부터 물려받은 매핑 정보를 재정의하려면 @AttributeOverride 를 사용할 수 있다.

    @Entity
    @AttributeOverride(name="id", column=@Column(name="MEMBER_ID"))
    public class Member extends BaseEntity { ... }
    
    • 부모에게 상속받은 id 속성의 컬럼명을 MEMBER_ID 로 재정의했다.
    @Entity
    @AttributeOverrides(
    	@AttributeOverride(name="id", column=@Column(name="MEMBER_ID")),
      @AttributeOverride(name="name", column=@Column(name="MEMBER_NAME"))
    )
    public class Member extends BaseEntity { ... }
    
    • 둘 이상의 속성 매핑 정보를 재정의할 수 있다.
  • @MappedSuperclass 로 지정한 클래스는 엔티티가 아니므로 JPQL에서 사용할 수 없다.

    • 이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것이 좋다.
  • @MappedSuperclass 를 사용해서 createdAt , modifiedAt 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있다.

복합 키와 식별 관계 매핑

식별 관계 vs 비식별 관계

  • 데이터베이스 테이블 사이에 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별 관계와 비식별 관계로 구분한다.

식별 관계

  • 식별 관계는 부모 테이블의 기본 키를 내려 받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계다.

비식별 관계

  • 비식별 관계는 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계다.

  • 비식별 관계는 외래 키에 NULL 을 허용하는지에 따라 필수적 비식별 관계와 선택적 비식별 관계로 나눈다.

    • 필수적 비식별 관계 : 외래 키에 NULL 을 허용하지 않는다. 즉, 연관관계를 필수적으로 맺어야 한다.
    • 선택적 비식별 관계 : 외래 키에 NULL 을 허용한다. 즉, 연관관계를 맺을지 말지 선택할 수 있다.

복합 키 : 비식별 관계 매핑

  • JPA에서 식별자가 두 개 이상일 때는 별도의 식별자 클래스를 만들어야 한다.
  • JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용한다.
    • 식별자를 구분하기 위해 equalshashCode 를 사용해서 동등성 비교를 한다.
    • 식별자 필드가 하나일 때는 보통 자바의 기본 타입을 사용하므로 문제가 없지만, 2개 이상일 때는 별도의 식별자 클래스를 만들고 equalshashCode 를 구현해야 한다.
  • JPA는 복합 키를 지원하기 위해 @IdClass@EmbeddedId 2가지 방법을 제공한다.

@IdClass

  • PARENTCHILD 테이블이 있고, PARENT 테이블의 기본 키를 PARENT_ID1 , PARENT_ID2 로 묶은 복합 키로 구성했다고 하자.
    • CHILD 테이블은 PARENT 테이블의 기본 키를 외래 키로 사용하는 비식별 관계이다.
@Entity
@IdClass(ParentId.class)
public class Parent {
  @Id
  @Column(name="PARENT_ID1")
  private String id1;
  
  @Id
  @Column(name="PARENT_ID2")
  private String id2;
  
  ...
}
  • 각각의 기본 키 컬럼을 @Id 로 매핑하고, @IdClass 를 사용해 ParentId 클래스를 식별자 클래스로 지정했다.
public class ParentId implements Serializable {
  private String id1;
  private String id2;
  
  public ParentId() {}
  
  public ParentId(String id1, String id2) {
		this.id1 = id1;
    this.id2 = id2;
  }
  
  @Override
  public boolean equals(Object o) { ... }
  
  @Override
  public int hashCode() { ... }
}
  • @IdClass 를 사용할 때 식별자 클래스는 다음 조건을 만족해야 한다.
    • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
    • Serializable 인터페이스를 구현해야 한다.
    • equals , hashCode 를 구현해야 한다.
    • 기본 생성자가 있어야 한다.
    • 식별자 클래스는 public 이어야 한다.
저장
Parent parent = new Parent();
parent.setId1("myId1");
parent.setId2("myId2");
em.persist(parent);
  • 엔티티를 저장하는 코드에서 ParentId 클래스는 따로 사용되지 않는다.
  • persist() 메소드를 호출하면 영속성 컨텍스트에 엔티티를 등록하기 직전에 내부에서 Parent.id1 , Parent.id2 값을 사용해서 식별자 클래스인 ParentId 인스턴스를 생성하고 영속성 컨텍스트의 키로 사용한다.
조회
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);
  • 식별자 클래스인 ParentId 인스턴스를 생성해서 엔티티를 조회하는 조건으로 사용한다.
자식 클래스
@Entity
public class Child {
  @Id
  private String id;
  
  @ManyToOne
  @JoinColumns({
    @JoinColumn(name="PARENT_ID1", referencedColumnName="PARENT_ID1"),
    @JoinColumn(name="PARENT_ID2", referencedColumnName="PARENT_ID2")
  })
  private Parent parent;
}
  • 부모 테이블의 기본 키 컬럼이 복합 키이므로 자식 테이블의 외래 키도 복합 키다.
  • 외래 키 매핑시 여러 컬럼을 매핑해야 하므로 @JoinColumns 를 사용한다.
    • 각각의 외래 키 컬럼은 @JoinColumn 으로 매핑한다.

@EmbeddedId

  • @IdClass 가 데이터베이스에 맞춘 방법이라면 @EmbeddedId 는 좀 더 객체지향적인 방법이다.
@Entity
public class Parent {
  @EmbeddedId
  private ParentId id;
  
  private String name;
  ...
}
  • Parent 엔티티에서 식별자 클래스를 직접 선언해 @EmbeddedId 어노테이션을 사용해준다.
@Embeddable
public class ParentId implements Serializable {
  @Column(name="PARENT_ID1")
  private String id1;
  
  @Column(name="PARENT_ID2")
  private String id2;
  
  // equals and hashCode 구현
  ...
}
  • @EmbeddedId 를 적용한 식별자 클래스는 식별자 클래스에 기본 키를 직접 매핑한다.
  • @EmbeddedId 를 적용한 식별자 클래스는 다음 조건을 만족해야 한다.
    • @Embeddable 어노테이션을 선언해야 한다.
    • Serializable 인터페이스를 구현해야 한다.
    • equals , hashCode 를 구현해야 한다.
    • 기본 생성자가 있어야 한다.
    • 식별자 클래스는 public 이어야 한다.
저장
Parent parent = new Parent();
ParentId parentId = new ParentId("myId1", "myId2");
parent.setId(parentId);
parent.setName("parentName");
em.persist(parent);
  • 엔티티를 저장하는 코드에서 식별자 클래스 ParentId 인스턴스를 직접 생성해서 사용한다.
조회
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);
  • 엔티티를 조회하는 코드에서도 식별자 클래스 ParentId 인스턴스를 생성해서 조회 조건으로 사용한다.

@IdClass vs @EmbeddedId

  • @EmbeddedId@IdClass 와 비교해서 더 객체지향적이고 중복도 없는 장점이 있지만 특정 상황에 JPQL이 조금 더 길어질 수 있다.

    em.createQuery("select p.id.id1, p.id.id2 from Parent p");  // @EmbeddedId
    em.createQuery("select p.id1, p.id2 from Parent p");				// @IdClass
    

복합 키 : 식별 관계 매핑

  • 식별 관계에서 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야 하므로 @IdClass@EmbeddedId 를 사용해서 식별자를 매핑해야 한다.

  • 아래와 같이 부모, 자식, 손자까지 기본 키를 계속 전달하는 식별 관계를 생각해보자.

    image

@IdClass와 식별 관계

@Entity
public class Parent {
  @Id
  @Column(name="PARENT_ID")
  private String id;
  ...
}

@Entity
@IdClass(ChildId.class)
public class Child {
  @Id
  @ManyToOne
  @JoinColumn(name="PARENT_ID")
  public Parent parent;
  
  @Id
  @Column(name="CHILD_ID")
  private String childId;
  ...
}

public class ChildId implements Serializable {
  private String parent;
  private String childId;
  
  // equals, hashCode
  ...
}

@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
  @Id
  @ManyToOne
  @JoinColumns({
    @JoinColumn(name="PARENT_ID"),
    @JoinColumn(name="CHILD_ID")
  })
  private Child child;
  
  @Id
  @Column(name="GRANDCHILD_ID")
  private String id;
  ...
}

public class GrandChildId implements Serializable {
  private ChildId child;
  private String id;
  
  // equals, hashCode
  ...
}
  • 식별 관계는 기본 키와 외래 키를 같이 매핑해야 한다.
    • 식별자 매핑인 @Id 와 연관관계 매핑인 @ManyToOne 을 같이 사용한다.

@EmbeddedId와 식별 관계

  • @EmbeddedId 로 식별 관계를 구성할 때는 @MapsId 를 사용해야 한다.
@Entity
public class Parent {
  @Id
  @Column(name="PARENT_ID")
  private String id;
  
  private String name;
  ...
}

@Entity
public class Child {
  @EmbeddedId
  private ChildId id;
  
  @MapsId("parentId")
  @ManyToOne
  @JoinColumn(name="PARENT_ID")
  public Parent parent;
  
  private String name;
  ...
}

@Embeddable
public class ChildId implements Serializable {
  private String parentId;
  
  @Column(name="CHILD_ID")
  private String id;
  
  // equals, hashCode
  ...
}

@Entity
public class GrandChild {
  @EmbeddedId
  private GrandChildId id;
  
  @MapsId("childId")
  @ManyToOne
  @JoinColumns({
    @JoinColumn(name="PARENT_ID"),
    @JoinColumn(name="CHILD_ID")
  })
  private Child child;
  
  private String name;
 	...
}

@Embeddable
public class GrandChildId implements Serializable {
  private ChildId childId;
  
  @Column(name="GRANDCHILD_ID")
  private String id;
  
  // equals, hashCode
  ...
}
  • @EmbeddedId 는 식별 관계로 사용할 연관관계의 속성에 @MapsId 를 사용한다.
    • @MapsId 는 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 의미이다.
    • @MapsId 의 속성 값은 식별자 클래스의 기본 키 필드를 지정하면 된다.

비식별 관계로 구현

  • CHILD 테이블과 GRANDCHILD 테이블에서 부모 테이블로부터 받은 기본 키를 외래 키로만 사용함으로써 식별 관계를 비식별 관계로 변경할 수 있다.
@Entity
public class Parent {
  @Id
  @GeneratedValue
  @Column(name="PARENT_ID")
  private Long id;
  private String name;
  ...
}

@Entity
public class Child {
  @Id 
  @GeneratedValue
  @Column(name="CHILD_ID")
  private Long id;
  private String name;
  
  @ManyToOne
  @JoinColumn(name="PARENT_ID")
  private Parent parent;
  ...
}

@Entity
public class GrandChild {
  @Id
  @GeneratedValue
  @Column(name="GRANDCHILD_ID")
  private Long id;
  private String name;
  
  @ManyToOne
  @JoinColumn(name="CHILD_ID")
  private Child child;
  ...
}
  • 식별 관계의 복합 키를 사용한 코드와 비교할 때 훨씬 매핑도 쉽고 코드도 간단하다.
    • 복합 키가 없으므로 복합 키 클래스를 만들지 않아도 된다.

일대일 식별 관계

image

  • 일대일 식별 관계는 자식 테이블의 기본 키 값으로 부모 케이블의 기본 키 값만 사용한다.
    • 부모 테이블의 기본 키가 복합 키가 아니면 자식 테이블의 기본 키도 복합 키로 구성하지 않아도 된다.
@Entity
public class Board {
  @Id
  @GeneratedValue
  @Column(name="BOARD_ID")
  private Long id;
  
  private String title;
  
  @OneToOne(mappedBy="board")
  private BoardDetail boardDetail;
  ...
}

@Entity
public class BoardDetail {
  @Id
  private Long boardId;
  
  @MapsId
  @OneToOne
  @JoinColumn(name="BOARD_ID")
  private Board board;
  
  private String content;
  ...
}
  • 식별자가 필드 하나인 경우 @MapsId 를 선언하고 속성 값은 비워두면 된다.
    • @Id 를 사용해서 기본 키로 지정한 BoardDetail.boardId 와 매핑된다.

식별, 비식별 관계의 장단점

  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어나게 된다.
    • 조인할 때 SQL이 복잡해지고, 기본 키 인덱스가 불필요하게 커질 수 있다.
  • 식별 관계를 사용할 때 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많다.
    • 비즈니스 요구사항은 변할 수 있기 때문에 부모 테이블의 자연 키 컬럼들이 자식 테이블까지 전파되면 변경하기에 힘들다.
  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하므로 테이블 구조가 유연하지 못하다.
  • 식별 관계는 2개 이상의 컬럼을 묶은 복합 기본 키를 사용하기 때문에 JPA에서 복합 키 클래스를 만들어 사용해야 한다.
    • 단일 기본 키를 매핑하는 것보다 많은 노력이 필요하다.
  • 비식별 관계는 주로 대리 키를 사용하는데 JPA의 @GeneratedValue 처럼 대리 키를 자동 생성해주는 기능을 사용하기 용이하다.
  • 식별 관계는 기본 키 인덱스를 활용하기 좋고, 특정 상황에 부모 테이블과의 조인 없이 하위 테이블만으로 검색을 완료할 수 있다는 장점도 있다.
  • 될 수 있으면 비식별 관계를 사용하고 기본 키는 Long 타입의 대리 키를 사용하는 것이 좋다.
    • 또한 선택적 비식별 관계보다는 필수적 비식별 관계를 사용하는 것이 좋다.
    • 선택적 비식별 관계는 NULL 을 허용하므로 조인할 때 outer join을 사용해야 하는 반면, 필수적 비식별 관계는 NOT NULL 로 항상 관계가 있다는 것을 보장하므로 inner join만 사용해도 된다.

조인 테이블

  • 데이터베이스 테이블의 연관관계를 설계하는 방법은 조인 컬럼 사용 , 조인 테이블 사용 두 가지가 있다.
  • 주로 테이블 간에 관계는 외래 키 컬럼을 사용해서 관리한다.
  • 예를 들어 회원과 사물함의 관계를 생각해보자.
    • 각각 테이블에 데이터를 등록했다가 회원이 원할 때 사물함을 선택할 수 있다고 가정하자.
    • 회원이 사물함을 선택하기 전까지는 둘 사이에 관계가 없으므로 MEMBER 테이블의 LOCKER_ID 외래 키에 null 값을 입력해두어야 한다.
      • 이렇게 외래 키에 null 값을 허용하는 관계를 선택적 비식별 관계라고 한다.
    • 선택적 비식별 관계는 회원과 사물함을 조인할 때 OUTER JOIN을 사용해야 한다.
      • INNER JOIN을 사용하면 사물함과 관계가 없는 회원은 조회되지 않기 때문이다.
    • 회원과 사물함이 아주 가끔 관계를 맺는다면 외래 키 값이 대부분 NULL 로 저장되는 단점이 있다.
  • 조인 테이블을 사용해서 연관관계를 관리하면 두 테이블의 외래 키를 가지고 연관관계를 관리하게 된다.
    • MEMBERLOCKER 에는 연관관계를 관리하기 위한 외래 키 컬럼이 필요하지 않다.
    • 회원이 원할 때 사물함을 선택하면 조인 테이블인 MEMBER_LOCKER 테이블에만 값을 추가하면 된다.

조인 테이블은 테이블을 하나 더 추가해야 하고, 연관관계인 두 테이블을 조인하기 위해서 조인 테이블까지 함께 조인해야 한다는 단점이 있다.

일대일 조인 테이블

  • 일대일 관계를 만들려면 조인 테이블의 키 컬럼 각각에 UNIQUE 제약 조건을 걸어야 한다.
@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;
  ...
}
  • 부모 엔티티에서 @JoinColumn 대신에 @JoinTable 을 사용하여 조인 테이블을 지정해주었다.

    • name : 매핑할 조인 테이블 이름
    • joinColumns : 현재 엔티티를 참조하는 외래 키
    • inverseJoinColumns : 반대방향 엔티티를 참조하는 외래 키
  • 양방향으로 매핑하려면 다음 코드를 추가하면 된다.

    public class Child {
      ...
      @OneToOne(mappedBy="child")
      private Parent parent;
    }
    

일대다 조인 테이블

  • 일대다 관계를 만드려면 조인 테이블의 컬럼 중 다(N) 쪽 컬럼인 CHILD_IDUNIQUE 제약 조건을 걸어야 한다.
@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"),
             inverseColumns=@JoinColumn(name="CHILD_ID"))
  private List<Child> child = new ArrayList<Child>();
  ...
}

@Entity
public class Child {
  @Id
  @GeneratedValue
  @Column(name="CHILD_ID")
  private Long id;
  private String name;
  ...
}

다대일 조인 테이블

@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>();
  ...
}

@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"),
             inverseColumns=@JoinColumn(name="PARENT_ID"))
  private Parent parent;
  ...
}

다대다 조인 테이블

  • 다대다 관계를 만드려면 조인 테이블의 두 컬럼을 합해서 하나의 복합 유니크 제약조건을 걸어야 한다.
@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"),
             inverseColumns=@JoinColumn(name="CHILD_ID"))
  private List<Child> child = new ArrayList<Child>();
  ...
}

@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;
  ...
}
  • @SecondaryTable 을 사용해서 BOARD_DETAIL 테이블을 추가로 매핑했다.
    • name : 추가로 매핑할 테이블의 이름
    • pkJoinColumns : 추가로 매핑할 테이블의 기본 키 컬럼 속성
  • content 필드는 @Column(table="BOARD_DETAIL") 를 사용해서 BOARD_DETAIL 테이블의 컬럼에 매핑했다.
    • 테이블을 지정하지 않으면 기본 테이블인 BOARD 테이블에 매핑된다.
  • 이 방법은 항상 두 테이블을 조회하므로 최적화하기 어려워 권장하지 않는다.