JPA 다양한 연관관계
연관관계 매핑시 고려사항
다중성
- 다대일 : @ManyToOne
- 일대다 : @OneToMany
- 일대일 : @OneToOne
- 다대다 : @ManyToMany
다대다의 경우 실무에서 사용을 지양한다.
단방향, 양방향
- 테이블 : 외래키 하나로 양쪽 조인 가능하다. 방향이라는 개념이 없다.
- 객체 : 참조용 필드가 있는 쪽으로만 참조 가능하다. 한쪽만 참조하면 단방향, 양쪽이 서로 참조하면 양방향이다.
연관관계의 주인
- 테이블은 외래키 하나로 두 테이블이 연관관계를 맺는다.
- 객체 양방향 관계는
A->B
,B->A
처럼 참조가 두 개인 것이다. 그래서 둘 중 테이블의 외래키를 관리할 곳을 지정해야 한다(연관관계의 주인 설정).
다대일
가장 많이 사용하는 연관관계. DB 테이블의 N:1 관계에서 외래키는 항상 N쪽에 존재한다. 객체의 양방향 관계에서 연관관계의 주인은 항상 N쪽(외래키 존재로 관리가 편함).
다대일 단방향 [N:1]
@Entity
public class Member {
...
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
...
}
다대일 양방향 [N:1, 1:N]
객체의 양방향 관계에서 실선이 연관관계의 주인, 점선은 연관관계의 주인이 아님을 표시한다.
@Entity
public class Member {
...
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// 편의 메서드
public void setTeam(Team team) {
this.team = team;
//무한 루프에 빠지지 않도록 체크
if(!team.getMembers().contains(this)){
team.getMembers().add(this);
}
}
...
}
@Entity
public class Team {
...
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
// 편의 메서드
public void addMember(Member member) {
this.members.add(member);
//무한 루프에 빠지지 않도록 체크
if(member.getTeam() != this) {
member.setTeam(this);
}
}
...
}
일대다
다대일 관계의 반대 방향 연관관계. 엔티티를 하나 이상 참조할 수 있으므로 자바 컬렉션을 사용한다.
일대다 단방향 [1:N]
- 1:N에서 1이 연관관계의 주인이 된다. 외래키는 N 쪽에 존재한다.
- 객체와 테이블의 차이 때문에 반대편 테이블의 외래키를 관리하는 특이한 구조가 형성된다.
@JoinColumn
을 사용해야한다. 그렇지 않으면 조인 테이블 방식을 사용하여 중간에 연결 테이블이 추가된다.
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
...
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
...
}
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
private String username;
...
}
일대다 단방향 매핑은 매핑한 객체가 관리하는 외래키가 다른 테이블에 있다. 즉, Team 엔티티에서 members로 연관관계를 관리한다는 말이다. 이 때 UPDATE SQL 추가 실행이 필요하므로 성능, 관리 측면에서 좋지 않다. 이를 해결하기 위한 가장 좋은 해결방법은 일대다 단방향 매핑을 다대일 양방향 매핑으로 수정하는 것이다.
일대다 양방향 [1:N, N:1]
- 양방향 매핑에서
@OneToMany
는 연관관계의 주인이 될 수 없다. 관계형 데이터베이스 특성상 N쪽에 외래키가 존재하며 항상 N쪽에@ManyToOne
을 사용하기 때문이다. 그래서 이런 매핑방법은 공식적으로 존재하지 않는다. - 일대다 양방향 매핑이 완전히 불가능하지는 않다. 다대일 단방향에
@JoinColumn(insertable = false, updatable = false)
(읽기 전용 속성)을 설정하여 양방향처럼 사용하는 방법이 있다. 그래도 가능한 다대일 양방향을 사용하는 것이 좋다.
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
...
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
...
}
@Entity
public class Member {
...
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
...
}
일대일 [1:1]
- 테이블 관계에서 일대다, 다대일은 항상 N쪽에 외래키를 가진다. 그런데 일대일 관계에서는 주테이블, 대상테이블 어느 곳에서든 외래키를 가질 수 있다.
- 외래키에 DB 유니크(UNI) 제약조건을 추가해야 한다.
주 테이블 외래키
- 주테이블에 외래키를 두고 대상 테이블을 참조한다. 외래키를 객체 참조와 비슷하게 사용할 수 있다.
- 주 테이블이 외래키를 가짐으로써 주테이블을 통해 대상 테이블 관계를 확인할 수 있다는 장점을 가진다.
주 테이블 외래키 단방향
다대일 단방향(@ManyToOne
) 매핑과 유사하다.
@Entity
public class Member {
...
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
...
}
@Entity
public class Locker {
@Id
@GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
...
}
주 테이블 외래키 양방향
다대일 양방향 매핑처럼 외래키가 있는 곳이 연관관계의 주인이다. 반대편은 mappedBy
속성의 값으로 주인을 지정한다.
@Entity
public class Member {
...
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
...
}
@Entity
public class Locker {
@Id
@GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "locker")
private Member member;
...
}
대상 테이블 외래키
테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있다는 장점을 가진다.
대상 테이블 외래키 단방향
일대일 관계에서 대상 테이블에 외래키가 있는 단방향 관계는 JPA에서 지원하지 않는다. 그래서 양방향 관계를 설정해야 가능하다.
대상 테이블 외래키 양방향
사실상 일대일 주 테이블 외래키 양방향과 같다.
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@OneToOne(mappedBy = "member")
private Locker locker;
...
}
@Entity
public class Locker {
@Id
@GeneratedValue
@Column(name = "LOCKER_ID")
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
...
}
다대다 [N:N]
객체는 컬렉션을 사용하여 다대다 관계를 표현할 수 있지만, RDB에서는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 연결 테이블을 추가하여 일대다, 다대일 관계로 형성해야 한다.
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID");
private String id;
private String username;
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT",
joinColumns = @JoinColumn(name = "MEMBER_ID"),
inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
private List<Product> products = new ArrayList<>();
...
}
@Entity
public class Product {
@Id
@Column(name = "PRODUCT_ID")
private String id;
private String name;
...
}
@JoinTable
속성 | 기능 |
---|---|
name | 조인 테이블 명 |
joinColumns | 현재 엔티티를 참조하는 외래키 |
inverseJoinColumns | 반대방향 엔티티를 참조하는 외래키 |
다대다 매핑 한계
@ManyToMany
로 연결테이블을 처리하면 편리해보이지만 실무에서 사용하기 어렵다. 연결 테이블에 단순히 식별자만 담는 것이 아닌 다른 컬럼들이 필요하게 될 수도 있기 때문이다.
다대다 매핑 한계 극복
연결 테이블용 엔티티를 추가하여 @ManyToMany
를 @ManyToOne
, @OneToMany
로 분리하여 해결할 수 있다.
- 식별관계 : 받아온 식별자를 복합키로 설정
- 비식별관계 : 받아온 식별자는 외래키로만 사용하고, 새로운 식별자를 추가
비식별관계를 사용하면 복합키 없이 단순하고 편리한 ORM 매핑이 가능하다는 장점이 있다. 그러나 아래 예제는 복합키 설정방법을 나타내고자 식별관계를 사용했다.
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
//역방향
@OneTomany(mappedBy = "member")
private List<MemberProduct> memberProducts;
...
}
@Entity
public class Product {
@Id
@Column(name = "PRODUCT_ID")
private String id;
...
}
@Entity
@IdClass(MemberProductId.class) // 복합키 설정
public class MemberProduct {
@Id
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member; //MemberProductID.member와 연결
@Id
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product; //MemberProductId.product와 연결
private int orderAmount;
...
}
// 복합키 식별자 클래스
public class MemberProductId implements Serializable {
private String member;
private String product;
...
}
복합키 식별자 클래스
JPA에서 복합키를 사용하려면 별도 식별자 클래스를 생성해야한다.
- Serializable 구현
- equals와 hashCode 메서드를 구현
- 기본 생성자 필요
- public 클래스
@IdClass
or@EmbeddedId
애니테이션으로 식별자 클래스를 지정
Comments