교재: 자바 ORM 표준 JPA 프로그래밍
6장은 다양한 연관관계를 다룬다 (다대일, 일대다, 일대일, 다대다).
ex: 회원(N), 팀(1)이 있으면 회원 쪽이 연관관계의 주인이다.
// 회원 엔티티
@Entity
public class Member {
@Id @GeneratedValue
@Column(name="MEMBER_ID"
private Long id;
private String username;
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
// ...
}
// 팀 엔티티
@Entity
public class Team {
@Id @GeneratedValue
@Column(name="TEAM_ID")
private Long id;
private String name;
// ...
}
→ 회원은 Member.team으로 팀 엔티티를 참조 할 수 있다. 팀에는 회원을 참조하는 필드가 없다. 회원과 팀은 다대일 단방향 연관관계다.
// 회원 엔티티
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
// 팀 엔티티
@OneToMany(mappedBy="team")
private List<Member> members = new ArrayList<Member>();
Member.team : 외래 키(TEAM_ID)를 관리하는 연관관계의 주인이다.Team.members : mappedBy로 매핑된 주인이 아닌 쪽이다. 조회와 객체 그래프 탐색에만 사용한다.일대다 관계는 다대일 관계의 반대 반향이다. 일대다 관계는 엔티티를 하나 이상 참조할 수 있어서 자바 컬렉션인 Collection, List, Set, Map 중에 하나를 사용해야 한다.
하나의 팀은 여러 회원을 참조할 수 있는데 이런 관계를 일대다 관계라 한다. 팀은 회원들을 참조하지만 회원은 팀을 참조하지 않으면 둘의 관계는 일대다 단방향 관계다.
// 팀 엔티티
@OneToMany
// 일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 한다.
@JoinColumn(name="TEAM_ID") // MEMBER 테이블의 TEAM_ID (FK)
private List<Member> members = new ArrayList<Member>()'
// 회원
@Id @GeneratedValue
@Column(name="MEMBER_ID")
private Long id;
private String username;
team_id를 변경해야 해서 INSERT 이후에 추가 UPDATE 쿼리가 발생한다 → 비효율적일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자
일대다 양방향 매핑은 일반적으로 존재하지 않는다. 양방향 매핑에서 연관관계의 주인은 항상 외래 키를 가진 쪽이고, @OneToMany는 주인이 될 수 없고 @ManyToOne에는 mappedBy 속성이 없다.
다만 아래와 같이 일대다를 주인처럼 사용하는 방식으로 “형식적인 양방향” 구현은 가능하다.
// 팀 엔티티
@OneToMany
@JoinColumn(name="TEAM_ID")
private List<Member> members = new ArrayList<Member>()'
// 회원 엔티티
@ManyToOne
@JoinColumn(name="TEAM_ID", insertable=false, updatable=false)
private Team team;
될 수 있으면 다대일 양방향 매핑을 사용하자!
일대일 관계는 양쪽이 서로 하나의 관계만 가지는 구조다. (ex: 회원은 하나의 사물함만 사용하고, 사물함도 하나의 회원에 의해서만 사용된다.)
객체지향 개발자들은 주 테이블에 외래 키가 있는 것을 선호한다. JPA도 주 테이블에 외래 키가 있으면 더 직관적이고 편리하게 매핑할 수 있다.
// Member
@OneToOne
@JoinColumn(name="LOCKER_ID")
private Locker locker;
// Locker
@Id @GeneratedValue
@Column(name="LOCKER_ID")
private Long id;
객체 매핑에는 @OneToOne을 사용하고, @JoinColumn으로 외래 키를 지정한다. 이 경우 Member 테이블에 LOCKER_ID가 생성되어 Locker를 참조한다.
이 구조는 다대일 단방향과 거의 동일하게 동작하고 관계의 최대 개수가 1:1이라는 점만 다르다.
// Member
@OneToOne
@JoinColumn(name="LOCKER_ID")
private Locker locker;
// Locker
@OneToOne(mappedBy="locker")
private Member member;
MEMBER 테이블이 외래 키를 가지고 있어서 Member 엔티티에 있는 Member.locker가 연관관계의 주인이다.
반대 매핑인 사물한의 Locker.member는 mappedBy를 선언해서 연관관계의 주인이 아니라고 설정했다.
일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다.
JPA2.0부터 일대다 단방향 관계에서 대상 테이블에 외래 키가 있는 매핑을 허용했다. 하지만 일대일 단방향은 이런 매핑을 허용하지 않는다.
// Member
@OneToOne(mappedBy="member")
private Locker locker;
// Locker
@OneToOne
@JoinColumn(name="MEMBER_ID")
private Member member;
이 경우 대상 엔티티인 Locker가 연관관계의 주인이 되며, MEMBER_ID 외래 키를 통해 관계를 관리한다. Member는 mappedBy를 사용해 읽기 전용으로 관계를 조회만 한다.
// Member
@ManyToMany
@JoinTable(name="MEMBER_PRODUCT",
joinColumns = @JoinColumn(name="MEMBER_ID"),
inverseJoinColumns = @JoinColumn(name="PRODUCT_ID")
private List<Product> products = new ArrayList<Product>();
// Product
@Id @Column(name="PRODUCT_ID")
private String id;
회원 엔티티와 상품 엔티티를 @ManyToMany 로 매핑했다. 중요한 포인트: @ManyToMany 와 @JoinTable 을 사용해서 연결 테이블을 바로 매핑했다.
속성을 정리하면:
@JoinTable.name : 연결 테이블 지정@JoinTable.joinColumns : 현재 방향인 회원과 매핑할 조인 컬럼 정보 지정@JoinTable.inverseJoinColumns : 반대 방향인 상품과 매핑할 조인 컬럼 정보를 지정다대다 매핑이므로 역방향도 @ManyToMany 를 사용한다. 양쪽 중 원하는 곳에 mappedBy로 연관관계의 주인을 지정한다.
// 역방향 추가
// Product
@ManyToMany(mappedBy="products")
private List<Member> members;
@ManyToMany
@ManyToMany에서는 연결 테이블이 엔티티로 존재하지 않기 때문에 이러한 컬럼을 매핑할 수 없다.→ 이를 해결하기 위해 연결 테이블을 별도의 엔티티로 만들어 다대다 관계를 일대다, 다대일 관계로 풀어야 한다.
// Member
@Id @Column(name="MEMBER_ID")
private String id;
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts;
// Product
@Id @Column(name="PRODUCT_ID")
private String id;
private String name;
// MemberProduct (회원상품 엔티티)
@Id
@ManyToOne
@JoinColumn(name="MEMBER_ID")
private Member member; // MemberProductId.member와 연결
@Id
@ManyToOne
@JoinColumn(name="PRODUCT_ID")
private Product product; // MemberProductId.product와 연결
위 예제에서 MemberProduct 엔티티는 member와 product를 함께 기본 키로 사용하는 복합 키 구조를 가진다. 즉, MEMBER_ID와 PRODUCT_ID를 조합해 하나의 식별자로 사용하며, 같은 회원과 상품의 조합은 하나만 존재할 수 있다.
회원상품은 회원과 상품의 기본 키를 받아서 자신의 기본 키로 사용한다. 이처럼 부모 엔티티의 기본 키를 자식 엔티티의 기본 키로 함께 사용하는 관계를 식별관계(Identifying Relationship) 라고 한다.
→ 복합 키를 사용하는 방법이 복잡하다.
추천하는 방식은 데이터베이스에서 자동으로 생성되는 대리 키(Long 타입)를 기본 키로 사용하는 것이다. 기존의 복합 키 대신 ORDER_ID와 같은 단일 기본 키를 두고, MEMBER_ID, PRODUCT_ID는 외래 키로만 사용한다.
// Order
@Id @GeneratedValue
@Column(name="ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name="MEMBER_ID")
private Member member;
@ManyToOne
@JoinColumn(name="PRODUCT_ID")
private Product product;
// Member
@OneToMany(mappedBy="member")
private List<Order> orders = new ArrayList<Order>();
// Product
@Id @Column(name="PRODUCT_ID")
private String id;
private String name;
이 방식은 엔티티가 독립적인 식별자를 가지게 되어 식별관계가 아닌 비식별관계로 구성된다. 그 결과 매핑이 단순해지고, 코드 작성과 유지보수가 훨씬 수월해진다.
다대다 연관관계 정리
다대다 관계를 일대다 다대일 관계로 풀어내기 위해 연결 테이블을 만들 때 식별자를 어떻게 구성할지 선택해야 한다.