JPA (6) 연관 관계 매핑 - 2
06 Mar 2022김영한님의 자바 ORM 표준 JPA 프로그래밍 강의 정리
다대일 (N:1)
다대일 단방향
아래와 같이 두 개의 테이블이 있을 때, 여러 멤버가 팀에 속할 수 있다면 멤버 테이블에서 외래 키를 가지고 다대일의 관계를 가지게 된다.
Member | Team |
---|---|
member_id (PK) | team_id(PK) |
team_id (FK) | name |
객체에서 멤버에서만 팀을 참조하고 팀에서는 멤버를 참조하지 않는다면 다대일 관계에서 단방향으로만 연관 관계가 만들어진다.
// Member
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
// Team
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
}
다대일 단방향 관계는 가장 많이 사용되는 연관 관계이며 반대는 일대다 관계다.
다대일 양방향
양방향 관계는 멤버를 참조하지 않았던 팀에서 멤버를 참조할 수 있도록 추가해주면 된다.
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
일대다 (1:N)
일대다 단방향
일대다 관계에서는 1에 해당하는 쪽이 연관 관계의 주인이다. 테이블은 위와 같다고 가정했을 때 객체에서 연관 관계 매핑이 달라진다. @OneToMany 어노테이션으로 일대다를 나타내지만 1에 해당하는 쪽이 연관 관계의 주인 역할을 하기 때문에 @JoinColumn
을 꼭 추가해줘야 한다. @JoinColumn을 사용하지 않을 경우에는 JoinTable 방식을 사용하게 돼서 테이블이 하나 더 추가된다.
일대다 단방향은 엔티티가 관리하는 외래 키가 DB에서는 다른 테이블에 있어 연관 관계를 관리하기 위해서 추가 UPDATE 쿼리를 실행하게 된다. 일대다 단방향 보다는 다대일 양방향 매핑을 사용하는 것이 더 좋다.
// Team
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
@JoinColumn(name = "team_id")
private List<Member> members = new ArrayList<>();
}
// Member
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
}
일대다 양방향
일대다 양방향은 공식적으로 있지는 않지만 읽기 전용 필드를 이용해 양방향처럼 사용하는 방법이다. 반대편에도 @JoinColumn
을 추가해주는데 대신 insertable, updateable 옵션을 모두 false로 주도록 한다. 추천하는 방법은 아니므로 다대일 양방향 매핑을 사용하는 것이 좋다.
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
@JoinColumn(name = "team_id")
private List<Member> members = new ArrayList<>();
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "team_id", insertable=false, updatable=false)
private Team team;
}
일대일 (1:1)
일대일 관계는 반대도 일대일이다. 두 테이블 중 한 곳에서 외래 키를 선택하면 되고 외래 키에는 UNIQUE 조건을 추가하면 된다. 일대일 매핑은 다대일 단방향 매핑과 매우 유사하다. 멤버와 라커라는 테이블이 있을 때, 메인이 되는 테이블이 멤버라고 할 때, 멤버에 외래 키를 두고 객체에서도 멤버 클래스가 연관 관계의 주인이 될 수 있다.
Member | Locker |
---|---|
member_id (PK) | locker_id (PK) |
username | name |
locker_id (FK) |
// Member
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
@OneToOne
@JoinColumn(name = "locker_id")
private Locker locker;
}
// Locker
@Entity
class Locker {
@Id @GeneratedValue
@Column(name = "locker_id")
private Long id;
private String name;
}
여기서 양방향으로 할 경우에는 라커 클래스에도 멤버 객체를 참조할 수 있도록 필드를 추가하고 mappedBy를 적용하면 된다.
@Entity
public class Locker {
@Id @GeneratedValue
@Column(name = "locker_id")
private Long id;
private String name;
@OneToOne(mappedBy = "locker")
private Member member;
}
하지만, DB에서는 외래 키가 메인이 아닌 대상이 되는 라커에 존재하는데 객체에서는 멤버에서 외래 키를 관리하며 단방향으로 연관 관계를 설정하는 것은 불가능하다. 양방향으로 설정하는 것은 가능하다.
Member | Locker |
---|---|
member_id (PK) | locker_id (PK) |
username | name |
member_id(FK) |
// Member
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
@OneToOne(mappedBy = "member")
private Locker 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;
}
결국 일대일 관계는 외래 키를 메인 테이블에 두는지 대상 테이블에 두는지 두 가지 방법이 있다. 메인 테이블에 두게 되면 JPA 매핑이 편리하고 메인 테이블만 조회해도 대상 테이블의 데이터를 확인할 수 있다. 하지만 값이 없을 경우 외래 키에 null을 허용하게 된다. 대상 테이블에 외래 키를 둘 경우에는 일대일에서 일대다의 관계로 변경할 때도 테이블 구조를 유지할 수 있는 게 장점이지만 지연 로딩으로 설정해도 항상 즉시 로딩이 된다.
다대다 (N:M)
RDB에서는 정규화된 테이블 2개로는 다대다 관계를 표현할 수 없어서 두 테이블을 연결하는 테이블을 추가해야한다. 그리고 그 테이블과 일대다, 다대일 관계로 연결하게 된다. 객체에서는 컬렉션을 이용해서 두 개의 객체만으로도 다대다 관계를 표현할 수 있다. JPA에서는 @ManyToMany를 사용하고 @JoinTable로 연결 테이블을 지정한다. 단방향, 양방향 모두 지정할 수 있으며 양방향일 때는 mappedBy를 설정해준다.
@Entity
public class Member {
// 다른 필드
@ManyToMany
@JoinTable(name = "member_product")
private List<Product> products = new ArrayList<>();
}
@Entity
public class Product {
// 다른 필드
@ManyToMany(mappedBy = "products")
private List<Member> members = new ArrayList<>();
}
다대다 연결은 실무에서는 사용하지 않도록 해야한다. 연결 테이블이 단순하게 연결만 하는 것이 아니라 실무에서는 다른 상황도 생길 수 있고 중간에 테이블이 하나 더 있기 때문에 쿼리의 문제도 있다.
다대다의 문제를 해결하기 위해서는 연결 테이블을 엔티티로 사용해서 다대다를 일대다, 다대일 연결 관계로 풀면 된다. MemberProduct 엔티티가 추가되면서 이후에 추가로 필요한 필드가 생기면 추가할때도 더 쉽다.
@Entity
public class MemberProduct {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
}
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();
}
@Entity
public class Product {
@Id @GeneratedValue
@Column(name = "product_id")
private Long id;
@OneToMany(mappedBy = "product")
private List<MemberProduct> memberProducts = new ArrayList<>();
}