• 엔티티 연관관계를 매핑할 때는 3가지를 고려해야 한다.
    • 다중성
      • 다대일
      • 일대다
      • 일대일
      • 다대다
    • 단방향, 양방향
      • 객체 관계에서 한 쪽만 참조하는 것을 단방향 관계, 양쪽이 서로 참조한느 것을 양방향 관계라 한다.
    • 연관관계의 주인
      • 두 객체의 연관관계 중에서 외래 키를 관리하는 객체를 연관관계의 주인이라 한다.
      • 외래 키를 가진 테이블과 매핑한 엔티티가 외래 키를 관리하는 게 효율적이므로 보통 이곳을 연관관계의 주인으로 선택한다.

다대일

  • 일대다 또는 다대일 관계에서 외래 키는 항상 다쪽에 있다.
    • 따라서 객체 양방향 관계에서 연관관계의 주인은 항상 다쪽이다.

다대일 단방향 [N:1]

  • 회원은 팀 엔티티를 찹조할 수 있지만, 팀에는 회원을 참조하는 필드가 없다.
  • @JoinColumn(name = "TEAM_ID") 를 사용해서 Member.team 필드를 TEAM_ID 외래 키와 매핑
@Entity  
class Member(  
    var username: String,  
    @ManyToOne  
    @JoinColumn(name = "TEAM_ID")  
    var team: Team?,  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "MEMBER_ID")  
    var id: Long = 0,  
)

@Entity  
class Team(  
    var name: String,  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "TEAM_ID")  
    var id: Long = 0,  
)

다대일 양방향 [N:1, 1:N]

  • 양방향은 외래 키가 있는 쪽이 연관관계의 주인이다.
    • 주인이 아닌 Team.members는 조회를 위한 JPQL이나 객체 그래프를 탐색할 때 사용한다.
  • 양방향 연관관계는 항상 서로 참조해야 한다.
    • 편의 메소드는 양쪽에 다 작성하면 무한루프에 빠지므로 주의해야 한다.
@Entity  
class Member(  
    var username: String,  
    team: Team?,  
) {  
  
    @ManyToOne  
    @JoinColumn(name = "TEAM_ID")  
    var team = team  
       set(value) {  
          field = value  
          if (value != null && !value.members.contains(this)) { // 무한루프에 빠지지 않도록 체크  
             value.members.add(this)  
          }       }  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "MEMBER_ID")  
    var id: Long = 0  
}

@Entity  
class Team(  
    var name: String,  
) {  
  
    @OneToMany(mappedBy = "team")  
    val members: MutableList<Member> = mutableListOf()  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "TEAM_ID")  
    var id: Long = 0  
  
    fun addMember(member: Member) {  
       this.members.add(member)  
       if (member.team != this) {  
          member.team = this  
       }  
    }  
}

일대다

일대다 단방향 [1:N]

@Entity  
class Member(  
    var username: String,  
) {  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "MEMBER_ID")  
    var id: Long = 0  
}

@Entity  
class Team(  
    var name: String,  
) {  
  
    @OneToMany
    @JoinColumn(name = "TEAM_ID") // MEMBER 테이블의 TEAM_ID (FK)    
    val members: MutableList<Member> = mutableListOf()  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "TEAM_ID")  
    var id: Long = 0  
}
  • @JoinColumn을 명시하여 다쪽 테이블에 있는 외래 키를 명시해야된다.
    • 명시하지 않으면 JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용해서 매핑한다.
  • 일대다 단방향 매핑의 단점
    • 본인 테이블에 외래 키가 있으면 엔티티의 저장과 연관관계 처리를 INSERT SQL 한 번으로 끝낼 수 있지만, 다른 테이블에 외래 키가 있으면 연관관계 처리를 위한 UPDATE SQL를 추가로 실행해야 한다.
  • 일대다 단방향은 성능 문제도 있지만 관리도 부담이 된다.
    • 일대다 단방향 매핑 대신에 다대일 양방향 매핑을 사용하는 것이 좋다.
    • 다대일 양방향 매핑은 관리해야 하는 외래 키가 본인 테이블에 있기 때문이다.

일대다 양방향 [1:N, N:1]

  • 양방향 매핑에서 @OneToMany는 연관관계의 주인이 될 수 없다.
  • 거의 사용하지 않지만 일대다 양방향 매핑을 하려면, 일대다 단방향 매핑 반대편에 같은 외래 키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 하나 추가하면 된다
    • 반대편 다대일 쪽은 insertable = false, updateable = false로 설정해서 읽기만 가능하게 했다.
  • 이 방법은 일대다 단방향 매핑이 가지는 단점을 그대로 가지기 때문에 될 수 있으면 다대일 양방향 매핑을 사용하자.
@Entity  
class Member(  
    var username: String,  
    team: Team?  
) {  
  
    @ManyToOne  
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)  
    var team: Team? = team  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "MEMBER_ID")  
    var id: Long = 0  
}

@Entity  
class Team(  
    var name: String,  
) {  
  
    @OneToMany
    @JoinColumn(name = "TEAM_ID") // MEMBER 테이블의 TEAM_ID (FK)    
    val members: MutableList<Member> = mutableListOf()  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "TEAM_ID")  
    var id: Long = 0  
}

일대일 [ 1:1 ]

  • 일대일 관계는 다음과 같은 특징이 있다.
    • 일대일 관계는 그 반대도 일대일 관계다.
    • 일대일 관계는 주 테이블이나 대상 테이블 둘 중 어느 곳이나 외래 키를 가질 수 있다.
  • 주 테이블에 외래 키
    • 외래 키를 객체 참조와 비슷하게 사용할 수 있어서 객체지향 개발자들이 선호한다.
    • 장점: 주 테이블이 외래 키를 가지고 있으므로 주테이블만 확인해도 대상 테이블과 연관관계가 있는지 확인할 수 있다.
  • 대상 테이블에 외래 키
    • 전통적인 데이터베이스 개발자들이 선호한다.
    • 장점: 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있다.

주 테이블에 외래 키

단방향

  • MEMBER가 주 테이블이고 LOCKER는 대상 테이블이다.

@Entity  
class Member(  
    var username: String,  
    team: Team?,  
    @OneToOne  
    @JoinColumn(name = "LOCKER_ID")  
    var locker: Locker,  
) {  
  
    @ManyToOne  
    @JoinColumn(name = "team_id")  
    var team: Team? = team  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "MEMBER_ID")  
    var id: Long = 0  
}

@Entity  
class Team(  
    var name: String,  
) {  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "TEAM_ID")  
    var id: Long = 0  
}

양방향

  • MEMBER 테이블이 외래 키를 가지므로 연관관계의 주인이다.
@Entity  
class Locker(  
    var name: String,  
    @OneToOne(mappedBy = "locker")  
    var member: Member?  
) {  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "LOCKER_ID")  
    var id: Long = 0  
}

@Entity  
class Member(  
    var username: String,  
    team: Team?,  
    @OneToOne  
    @JoinColumn(name = "LOCKER_ID")  
    var locker: Locker,  
) {  
  
    @ManyToOne  
    @JoinColumn(name = "team_id")  
    var team: Team? = team  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "MEMBER_ID")  
    var id: Long = 0  
}

대상 테이블에 외래 키

단방향

  • 일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 JPA 에서 지원하지 않는다.
    • 단방향 관계를 Locker에서 Member 방향으로 수정하거나, 양방향 관계로 만들고 Locker를 연관관계의 주인으로 설정해야 한다.

양방향

@Entity  
class Member(  
    var username: String,  
    team: Team?,  
    @OneToOne(mappedBy = "member")  
    var locker: Locker,  
) {  
  
    @ManyToOne  
    @JoinColumn(name = "team_id")  
    var team: Team? = team  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "MEMBER_ID")  
    var id: Long = 0  
}

@Entity  
class Locker(  
    var name: String,  
    @OneToOne  
    @JoinColumn(name = "MEMBER_ID")  
    var member: Member?  
) {  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "LOCKER_ID")  
    var id: Long = 0  
}

다대다 [M:N]

  • 관계형 데이터베이스에서는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
    • 관계형 데이터베이스에서는 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.
  • 객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있다.

다대다: 단방향

  • @JoinTable.name: 연결 테이블을 지정한다.
  • @JoinTable.joinColumns: 현재 엔티티가 연결 테이블과 조인할 때 사용할 컬럼을 지정한다.
  • @JoinTable.inverseJoinColumns: 반대 방향 엔티티의 조인 컬럼 정보를 지정한다.
@Entity  
class Member(  
    var username: String,  
    @ManyToMany  
    @JoinTable(  
       name = "MEMBER_PRODUCT",  
       joinColumns = [JoinColumn(name = "MEMBER_ID")],  
       inverseJoinColumns = [JoinColumn(name = "PRODUCT_ID")]  
    )  
    var products: MutableList<Product> = mutableListOf()  
) {  
  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    @Column(name = "MEMBER_ID")  
    var id: Long = 0  
}

@Entity  
class Product(  
    var name: String,  
    @Id  
    @Column(name = "PRODUCT_ID")  
    var id: String  
)
  • 아래 코드를 실행하면 연결 테이블에도 값이 저장되는 것을 확인할 수 있다.

다대다: 양방향

  • 다대다 양방향에서는 연관관계 주인이 아닌곳에 @ManyToManymappedBy를 지정하면 된다.
@Entity  
class Product(  
    var name: String,  
    @Id  
    @Column(name = "PRODUCT_ID")  
    var id: String  
) {  
  
    @ManyToMany(mappedBy = "products")  
    var members: MutableList<Member> = mutableListOf()  
}
  • 양방향 연관관계는 편의 메소드를 추가해서 관리하는 것이 편리하다.
fun addProduct(product: Product) {  
    products.add(product)  
    if (!product.members.contains(this)) {  
       product.members.add(this)  
    }  
}

다대다: 매핑의 한계와 극복, 연결 엔티티 사용

  • 실무에서는 연결 테이블에 ORDERAMOUNT, ORDERDATE 같이 추가적인 컬럼이 필요한 경우가 있다.

  • 이런 경우에는 연결 테이블을 매핑하는 연결 엔티티를 만들고 이곳에 추가적인 컬럼을 매핑해야 된다.

    • 엔티티 간의 관계도 테이블 관계처럼 다대다에서 일대다, 다대일 관계로 풀어진다.
  • 복합 기본키를 사용하기 위해서 기본 키에 해당하는 필드에 @Id 로 매핑하고, @IdClass를 사용해 복합 기본 키를 매핑한다.

    • JPA에서 복합키를 사용하려면 별도의 식별자 클래스를 지정해야 된다.
    • 식별자 클래스는 아래의 조건을 만족해야 된다.
      • Serializable을 구현해야 한다.
      • equalshashCode 메소드를 구현해야 한다.
      • 기본 생성자가 있어야 한다.
      • 식별자 클래스는 public이어야 한다.
      • @IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있다.
@Entity  
class Member(  
    var username: String,  
    @Id  
    @Column(name = "MEMBER_ID")  
    var id: String,  
    @OneToMany(mappedBy = "member")  
    var products: MutableList<MemberProduct> = mutableListOf(),  
)

@Entity  
class Product(  
    var name: String,  
    @Id  
    @Column(name = "PRODUCT_ID")  
    var id: String  
)

@Entity  
@IdClass(MemberProductId::class)  
class MemberProduct(  
    @Id  
    @ManyToOne    
    @JoinColumn(name = "MEMBER_ID")  
    val member: Member,  
    @Id  
    @ManyToOne    
    @JoinColumn(name = "PRODUCT_ID")  
    val product: Product,  
    var orderAmount: Int,  
)  
  
data class MemberProductId(  
    var member: String? = null,  
    var product: String? = null,  
): Serializable
  • 식별 관계: 부모 테이블의 기본 키를 받아서 자신의 기본 키 + 외래 키로 사용하는 것
  • 아래 코드 실행 결과

다대다: 새로운 기본 키 사용

  • 추천하는 기본 키 생성 전략은 데이터베이스에서 자동으로 생성해주는 대리 키를 Long 값으로 사용하는 것이다.
    • 장점1: 간편하고 거의 영구히 쓸 수 있으며 비즈니스에 의존하지 않는다.
    • 장점2: 복합 키를 만들지 않아도 되므로 간단히 매핑을 완성할 수 있다.

다대다 연관관계 정리

  • 다대다 관계를 일대다 다대일 관계로 풀어내기 위해 연결 테이블을 만들 때 식별자를 어떻게 구성할지 선택해야 한다.
    • 식별 관계: 받아온 식별자를 기본 키 + 외래 키로 사용한다.
    • 비식별 관계: 받아온 식별자는 외래 키로만 사용하고 새로운 식별자를 추가한다.
  • 객체 입장에서보면 비식별 관계를 사용하는 것이 복합 키를 위한 식별자 클래스를 만들지 않아도 되므로 단순하고 편리하게 ORM 매핑을 사용할 수 있다.
    • 이런 이유로 식별 관계보다는 비식별 관계를 추천한다.