본문 바로가기

JPA

[개념] JPA 프로그래밍 - 06. 다양한 연관관계 매핑

다대일

다대일 단방향 [N:1]

@Entity
public class Member { 
 @Id
 @Column(name = "MEMBER_ID") 
 private Long id; 

 private String username; 

 @ManyToOne
 @JoinColumn(name="TEAM_ID") 
 private Team team; 
}


@Entity
public class Team {
 @Id
 @Column(name = "TEAM_ID") 
 private String id; 

 private String name; 
}

회원은 팀을 참조할 수 있지만 반대로 팀에는 회원을 참조하는 필드가 없다. 따라서 회원과 팀은 다대일 단방향 연관관계다.

 

다대일 양방향 [N:1, 1:N]

  • 양방향은 외래키가 있는 쪽이 연관관계의 주인이다.
    • 일대다와 다대일 연관관계는 항상 다(N)에 외래키가 있다.
  • 양방향 연관관계는 항상 서로를 참조해야 한다.
    • 항상 서로 참조하게 하려면 연관관계 편의 메소드를 작성하는 것이 좋은데 회원의 setTeam(), 팀의 addMember() 메소드가 편의 메소드들이다.
    • 편의 메소드는 한 곳에만 작성하거나 양쪽 다 작성할 수 있는데, 양쪽에 다 작성하면 무한루프에 빠지므로 주의해야 한다.
@Entity
public class Member { 
 @Id
 @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
 @Column(name = "TEAM_ID") 
 private String 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);
  }
 }           
}

일대다

일대다 단방향 [1:N]

@Entity
public class Team { 
  @Id
  @Column(name = "TEAM_ID") 
  private String 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
  @Column(name = "MEMBER_ID") 
  private Long id; 

  private String username;  
}

 

일대다 단방향 매핑의 단점

  • 매핑한 객체가 관리하는 외래키가 다른 테이블에 있다는 점
  • 본인 테이블에 외래키가 있으면 엔티티의 저장과 연관관계 처리를 INSERT SQL 한번만으로 끝낼 수 있지만, 다른 테이블에 외래키가 있으면 연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야 한다.
  • Member 엔티티는 Team 엔티티를 모른다. 그리고 연관관계에 대한 정보는 Team엔티티의 members가 관리한다.
    따라서 Member 엔티티를 저장할 때는 MEMBER 테이블의 TEAM_ID 외래키에 아무 값도 저장되지 않는다.
    대신 Team 엔티티를 저장할 때 Team.members의 참조 값을 확인해서 회원 테이블에 있는 TEAM_ID 외래키를 업데이트한다.
public void testSave() {
  Member member1 = new Member("member1"); 
  Member member2 = new Member("member2");

  Team team1 = new Team("team1");
  team1.getMembers.add(member1);
  team1.getMembers.add(member2);

  em.persist(member1); //INSERT - member1
  em.persist(member2); //INSERT - member2
  em.persist(team1);   //INSERT - team1, UPDATE - member1.fk, UPDATE - member2.fk

  transaction.commit();     
}

 

일대다 양방향 [1:N, N:1]

  • 일대다 양방향과 다대일 양방향은 사실 똑같은 말이다.
  • 연관관계의 주인은 항상 다 쪽인 @ManyToOne 을 사용한 곳이다.
  • 일대다 단방향 매핑 반대편에 다대일 단방향 매핑을 추가했다. 이때 일대다 단방향 매핑과 같은 TEAM_ID 외래키 컬럼을 매핑했다.
    이렇게 되면 둘 다 같은 키를 관리하므로 문제가 발생할 수 있다.
    따라서 반대편인 다대일 쪽은 insertable = false, updatable = false로 설정해서 읽기만 가능하게 했다.
  • 이 방법은 일대다 양방향 매핑이라기보다는 일대다 단방향 매핑 반대편에 다대일 단방향 매핑을 읽기 전용으로 추가해서 일대다 양방향처럼 보이도록 하는 방법이다.
    따라서 일대다 단방향 매핑이 가지는 단점을 그대로 가진다.
  • 될수 있으면 다대일 양방향 매핑을 사용하자.
@Entity
public class Team { 
  @Id
  @Column(name = "TEAM_ID") 
  private String id; 

  private String name; 

  @OneToMany
  @JoinColumn(name="TEAM_ID")
  private List<Member> members = new ArrayList<Member>();  
}

@Entity
public class Member { 
 @Id
 @Column(name = "MEMBER_ID") 
 private Long id; 

 private String username; 

 @ManyToOne
 @JoinColumn(name="TEAM_ID", insertable = false, updatable = false) 
 private Team team; 
}

일대일

  • 일대일 관계는 그 반대도 일대일
  • 주 테이블이나 대상 테이블 둘 중 어느 곳이나 외래키를 가질 수 있다.

주 테이블에 외래키

단방향

@Entity
public class Member { 
 @Id
 @Column(name = "MEMBER_ID") 
 private Long id; 

 private String username; 

 @OneToOne
 @JoinColumn(name="LOCKER_ID")
 private Locker locker;    
}

@Entity
public class Locker {
 @Id @GeneratedValue
 @Column(name="LOCKER_ID")
 private Long id;

 private String name;
}

 

양방향

  • MEMBER 테이블이 외래키를 가지고 있으므로 Member 엔티티에 있는 Member.locker가 연관관계의 주인이다.
@Entity
public class Member { 
 @Id
 @Column(name = "MEMBER_ID") 
 private Long id; 

 private String username; 

 @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
 @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;   
}

다대다

  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
  • 중간에 연결 테이블을 추가해야 한다. (예를 들어, Member(회원)과 Product(상품)테이블을 위해 Member_Product 연결 테이블 추가)
  • 객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있다.

다대다:단방향

@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을 사용해서 연결 테이블을 바로 매핑 => Member_Product 엔티티 없이 매핑을 완료
  • @JoinTable.name: 연결 테이블 지정
  • @JoinTable.joinColumns: 현재 방향인 회원과 매핑할 조인 컬럼 정보
  • @JoinTable.inverseJoinColumns: 반대 방향인 상품과 매핑할 조인 컬럼 정보
  • 회원을 저장할때 연결 테이블 Member_Product 값도 같이 저장해 줌

다대다:양방향

  • 역방향도 @ManyToMany 사용
  • 양쪽 중 원하는 곳에 mappedBy 로 연관관계의 주인을 지정한다. (mappedBy가 없는 곳이 연관관계의 주인)
@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(mappedBy="products")
 private List<Member> members;   
}

 

다대다:매핑의 한계와 극복, 연결 엔티티 사용

  • 보통 연결 테이블에 주문 수량 컬럼이나 주문한 날짜 같은 컬럼이 더 필요하다. => 이럴땐 @ManyToMany 사용 불가
    왜냐하면 주문 엔티티나 상품 엔티티에는 추가한 걸럼들을 매핑할 수 없기 때문이다.
  • 결국 연결테이블을 매핑하는 연결 엔티티를 만들어야 한다.

@Entity
public class Member { 
 @Id
 @Column(name = "MEMBER_ID") 
 private Long id; 

 @OneToMany(mappedBy="member")
 private List<MemberProduct> memberProducts;   
}

@Entity
public class Product {
 @Id
 @Column(name="PRODUCT_ID")
 private Long id;

 private String name;   
} 

//회원상품 엔티티 코드 (중요) 
@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;  // MemberProduct.member와 연결
 private String product;  // MemberProduct.product와 연결

 @Override
 public boolean equals(Object o) {...}

 @Override
 public int hashCode() {...}
}
  • MemberProduct 엔티티를 보면기본키를 매핑하는 @Id외래키를 매핑하는@JoinColumn을 동시에 사용해서 기본키+외래키 매핑
    @IdClass를 사용해서복합 기본키매핑

 ※ 복합 기본키 (복합키) ※
          - JPA에서 복합키를 사용하려면 별도의 식별자 클래스 생성해야함
          - Serializable을 구현해야 함
          - equals와 hashCode 메소드를 구현해야 함 (자바 IDE에는 대부분 자동으로 생성해줌)
          - 기본 생성자가 있어야 함
          - 식별자 클래스는 public 이어야 함
          - @IdClass를 사용하는 방법 외에 @EmbeddedId를 사용하는 방법도 있음

※ 식별관계 ※
          
- 회원상품은 회원과 상품의 기본키를 받아서 자신의 기본키로 사용함
            이렇게 부모 테이블의 기본키를 받아서 자신의 기본키 + 외래키로 사용하는 것을 식별관계라 한다.

 

다대다:새로운 기본키 사용

  • 복합키를 사용하는 방법은 복잡하다. 복합키를 사용하지 않고 간단히 다대다 관계를 구성하는 방법을 알아보자.
  • 추천하는 기본키 생성 전략은 데이터베이스에서 자동으로 생성해주는 대리키를 Long값으로 사용하는 것

@Entity
public class Member { 
 @Id
 @Column(name = "MEMBER_ID") 
 private Long id; 

 @OneToMany(mappedBy="member")
 private List<Order> orders = new ArrayList<Order>();   
}

@Entity
public class Product {
 @Id
 @Column(name="PRODUCT_ID")
 private Long id;

 private String name;   
} 

// Order로 이름 변경 
@Entity
public class 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;

 private int orderAmount;
}                
  • 식별자 클래스를 사용하지 않아서 코드가 한결 단순해짐

 

출처도서 : 자바 ORM 표준 JPA 프로그래밍 - 김영한 지음