6. 다양한 연관관계매핑
다대일 단방향 [N:1]
- 가장 많이 사용되는 연관관계
- 회원은 Member.team 팀 엔티티 참조가 가능하지만 팀은 회원을 참조하는 필드가 없음. 따라서 단방향 연관관계
// 회원 엔티티 @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; }
다대일 양방향 [N:1, 1:N]
- 실선이 연관관계의 주인(Member.team)
// 회원 엔티티 @Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; @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 { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<Member>(); public void addMember(Member member){ this.members.add(member); if(member.getTeam() != this) {// 무한루프에 빠지지 않도록 체크 member.setTeam(this); } } }
- 양방향 연관관계에서 연관관계의 주인은 외래키가 있는 엔티티가 주인
- 양방향 연관관계는 항상 서로를 참조해야 함
- 어느 한 쪽만 참조하도록 구현하지 말아야 함
- addMember, setTeam과 같은 연관관계 편의 메소드를 작성
- 이런 편의 메소드 작성 시 무한루프에 빠지지 않도록 주의해야 함
일대다 단방향 [1:N]
- 팀 엔티티의 members로 회원 테이블의 TEAM_ID 외래키를 관리
- 보통은 자신이 매핑한 테이블의 외래키를 관리하는게 일반적, 이 매핑은 반대쪽 테이블에 있는 외래키를 관리
- 일대다에서 외래키는 항상 “다” 쪽인 테이블에 있는데, 여기서 “다”에 해당하는 Member 엔티티에 외래키를 매핑할 참조 필드가 없기 때문
// 팀 엔티티 @Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany @JoinColumn(name = "TEAM_ID") // MEMBER 테이블의 TEAM_ID (FK) private List<Member> members = new ArrayList<Member>(); }
// 회원 엔티티 @Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; }
- 일대다 단방향 관게 매핑 시
@JoinColumn
을 반드시 명시해야 함- 없으면 JPA가 연관관계를 관리하는 조인 테이블(Join Table)을 만들어 연관관계를 매핑 함
- 일대다 단방향 매핑의 단점?
- 매핑한 객체가 관리하는 외래키가 다른 테이블에 있어서 엔티티의 저장과 연관관계 처리를 한 SQL에 할 수 없음
- 예를들어 아래와 같은 코드가 있을 때?
Member member = new Member("member1"); Team team = new Team("team1"); team.getMembers().add(member); em.persist(member); // INSERT member1 em.persist(team); // INSERT team1, UPDATE member1.fk
INSERT INTO MEMBER (MEMBER_ID, username) values (null, ?) INSERT INTO TEAM (TEAM_ID, name) values (null, ?) UPDATE MEMBER SET TEAM_ID=? WHERE MEMBER_ID=?
- Team 엔티티가 저장될 때 Member 엔티티의 외래키를 알 수 있기 때문에, update 쿼리를 한번 더 하게됨
- 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는게 낫다
- 다른 테이블의 외래키를 관리하는 것이 성능 문제, 유지보수의 문제가 있음
일대다 양방향 [1:N, N:1]
- 일대다 단방향 매핑에서 역참조가 가능하도록 읽기 전용 단방향 매핑을 만들어야 함
// 팀 엔티티 @Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany @JoinColumn(name = "TEAM_ID") // MEMBER 테이블의 TEAM_ID (FK) private List<Member> members = new ArrayList<Member>(); }
// 회원 엔티티 @Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; @ManyToOne @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) private Team team; }
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
- 반대편인 다대일 쪽은 읽기만 가능하도록 설정
- 일대다 단방향 매핑 반대편에 다대일 단방향 매핑을 읽기 전용으로 추가하는 것
일대일 [1:1]
- 외래키를 어디에 두냐에 따라 나뉘어짐
- 주 테이블에 외래키
- 외래키를 객체 참조와 비슷하게 사용할 수 있음
- 주 테이블이 외래키를 가지고 있음
- 대상 테이블에 외래키
- 전통적인 데이터베이스 개발자들이 선호하는 방식
- 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지 가능
- 주 테이블에 외래키 + 단방향
// Member 엔티티 @Entity public class Member{ @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; @OneToOne @JoinColumn(name="LOCKER_ID") Locker locker; }
// Locker 엔티티 @Entity public class Locker{ @Id @GeneratedValue @Column(name = "LOCKER_ID") private Long id; private String name; }
- 주 테이블에 외래키 + 양방향
- Member 엔티티는 단방향일 때와 동일
- 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에서 지원하지도 않고 매핑할 수 있는 방법도 없음…
- 대상 테이블에 외래키 + 양방향
// Member 엔티티 @Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; @OneToOne(mappedBy="member") Locker locker; }
// Locker 엔티티 @Entity public class Locker { @Id @GeneratedValue @Column(name = "LOCKER_ID") private Long id; private String name; @OneToOne @JoinColumn(name="MEMBER_ID") Member member; }
- 일대일 매핑에서 대상 테이블에 외래키를 두고 싶으면 이렇게 양방향으로 매핑하는 방법밖에 없음
다대다 [N:N]
- 실무에서 쓸 일이 없음
- 관계형 데이터베이스에서 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음
- 중간에 연결 테이블을 추가해서, 연결 테이블과 일대다, 다대일 관계를 유지해서 다대다 처럼 사용이 가능
- 그러나 객체를 사용할 땐 객체 2개로 다대다 관계 표현이 가능함
@ManyToMany를 사용해서 다대다 관계 매핑이 가능함
- 다대다 + 단방향
@Entity public class Member { @Id @Column(name = "MEMBER_ID") private Long 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<Product>(); }
@Entity public class Product { @Id @Column(name = "PRODUCT_ID") private Long id; private String name; }
- @ManyToMany, @JoinTable을 이용해서 연결 테이블 없이 바로 다대다 매핑
- @JoinTable
- @JoinTable.name : 연결 테이블 지정, 여기서는 MEMBER_PRODUCT 테이블 선택
- @JoinTable.joinColumns : 현재 방향인 회원과 매핑할 조인 컬럼 정보 지정, MEMBER_ID로 지정
- @JoinTable.inverseJoinColumns : 반대 방향인 상품과 매핑할 조인 컬럼 정보 지정, PRODUCT_ID로 지정
- 다대다 + 양방향
- 위에 단방향에서 역방향 참조만 추가해주면 됨
@Entity public class Product { @Id @Column(name = "PRODUCT_ID") private Long id; private String name; @ManyToMany(mappedBy = "products") // 역방향 매핑 추가 private List<Member> members }
- 다대다 매핑의 한계와 극복, 연결 엔티티 사용
- @ManyToMany는 실무에서 사용하기 힘든데, 비즈니스 모델을 다대다로 적용하다 보면 연결 테이블이 단순 연결만 하고 끝나지 않음
- 이렇게 연결 테이블에 주문수량, 주문시간같은 부가적인 데이터가 생길 수 밖에 없음
- 그러면 어떻게 하냐?
- 연결 테이블을 엔티티로 승격, @ManyToOne, @OneToMany로 관계를 나눔
- MemberProduct라는 엔티티를 새로 만들어서 회원의 주문과 상품의 중간에 위치
// Member 엔티티 @Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; private String username; @OneToMany private List<MemberProduct> memberProducts; }
@Entity public class Product { @Id @Column(name = "PRODUCT_ID") private String id; private String name; }
// MemberProduct 엔티티 @Entity @IdClass(MemberProductId.class) public class MemberProduct { @Id @GeneratedValue Column(name = "ORDER_ID") private Long 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; }