- 엔티티 연관관계를 매핑할 때는 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
)
- 아래 코드를 실행하면 연결 테이블에도 값이 저장되는 것을 확인할 수 있다.
다대다: 양방향
- 다대다 양방향에서는 연관관계 주인이 아닌곳에
@ManyToMany
의mappedBy
를 지정하면 된다.
@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
을 구현해야 한다.equals
와hashCode
메소드를 구현해야 한다.- 기본 생성자가 있어야 한다.
- 식별자 클래스는
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 매핑을 사용할 수 있다.
- 이런 이유로 식별 관계보다는 비식별 관계를 추천한다.