BOOK 3 - 자바 ORM 표준 JPA 프로그래밍(6)

다양한 연관관계 매핑

  • 연관관계를 매핑할 때는 다음 3가지를 고려해야 한다.
    • 다중성(multiplicity)
      • 다대일 ( @ManyToOne )
      • 일대다 ( @OneToMany )
      • 일대일 ( @OneToOne )
      • 다대다 ( @ManyToMany )
    • 단방향 / 양방향
    • 연관관계의 주인

다대일

  • 데이터베이스 테이블의 일대다 관계에서 외래 키는 항상 다(N) 쪽에 있다.
    • 객체 양방향 관계에서 연관관계의 주인은 항상 다쪽이다.

다대일 단방향 [N:1]

@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
  • 회원은 Member.team 으로 팀 엔티티를 조회할 수 있지만 반대로 팀에는 회원을 참조하는 필드가 없다.
  • 회원과 팀은 다대일 단방향 연관관계다.

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

image

  • 객체 연관관계에서 실선이 연관관계의 주인이고, 점선은 연관관계의 주인이 아니다.
  • 주인이 아닌 Team.members 는 조회를 위한 JPQL이나 객체 그래프 탐색에 사용한다.

일대다

  • 일대다 관계는 다대일 관계의 반대 방향이다.
  • 일대다 관계는 엔티티를 하나 이상 참조할 수 있으므로 자바 컬렉션인 Collection , List , Set , Map 중에 하나를 사용해야 한다.

일대다 단방향 [1:N]

  • 하나의 팀은 여러 회원을 참조할 수 있는데 이런 관계를 일대다 관계라 한다.
  • 팀은 회원들을 참조하지만 회원은 팀을 참조하지 않으면 단방향 연관관계다.
  • 일대다 관계에서 외래 키는 항상 다쪽 테이블에 있는데, 다(N) 쪽 엔티티에는 외래 키를 매핑할 수 있는 참조 필드가 없다.
    • 따라서 반대쪽 엔티티에서 반대편 다(N)쪽 테이블의 외래 키를 관리하게 된다.
  • 본인 테이블에 외래 키가 있으면 엔티티의 저장과 연관관계 처리를 INSERT SQL 한번으로 끝낼 수 있지만, 다른 테이블에 외래 키가 있으면 연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야 한다.
    • 참조 값을 확인해서 반대편 테이블에 있는 외래 키를 일일이 업데이트 해야 한다.

일대일 [1:1]

  • 일대일 관계는 그 반대도 일대일 관계다.
  • 테이블에서 일대일 관계는 주 테이블이나 대상 테이블 둘 중 어느 곳이나 외래 키를 가질 수 있다.
    • 따라서 주 테이블이나 대상 테이블 중에 누가 외래 키를 가질지 선택해야 한다.
  • 주 테이블에 외래 키
    • 주 객체가 대상 객체를 참조하는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 참조한다.
    • 외래 키를 객체 참조와 비슷하게 사용할 수 있어서 객체지향 개발자들이 선호한다.
    • 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있는 장점이 있다.
  • 대상 테이블에 외래 키
    • 전통적인 데이터베이스 개발자들이 선호한다.
    • 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있다는 장점이 있다.

주 테이블에 외래 키

단방향

  • 예를 들어 한 명의 회원이 하나의 사물함을 사용하는 상황을 가정해보자.

    @Entity
    public class Member {
      @Id
      @GeneratedValue
      @Column(name="MEMBER_ID")
      private Long id;
        
      @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 을 사용했다.

양방향

  • 양방향 연관관계에서는 연관관계의 주인을 정해야 한다.

  • MEMBER 테이블이 외래 키를 가지고 있으므로 Member 엔티티에 있는 Member.locker 가 연관관계의 주인이다.

    @Entity
    public class Locker {
      ...
      @OneToOne(mappedBy="locker")
      private Member member;
    }
    
    • Locker.membermappedBy 를 선언해서 연관관계의 주인 필드명을 명시해준다.

대상 테이블에 외래 키

단방향

  • 일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계는 JPA에서 지원하지 않는다.
    • 단방향 관계를 Locker -> Member 방향으로 수정하거나, 양방향 관계로 만들고 Locker 를 연관관계의 주인으로 설정해야 한다.

양방향

@Entity
public class Member {
  @Id
  @GeneratedValue
  @Column(name="MEMBER_ID")
  private Long id;
  
  @OneToOne(mappedBy="member")
  private Locker locker;
}

@Entity
public class Locker {
  @Id
  @GeneratedValue
  @Column(name="LOCKER_ID")
  private Long id;
  
  @OneToOne
  @JoinColumn(name="MEMBER_ID")
  private Member member;
  ...
}
  • 일대일 매핑에서 대상 테이블에 외래 키를 두고 싶으면 이렇게 양방향으로 매핑한다.
    • 대상 엔티티를 연관관계의 주인으로 만들어서 대상 테이블의 외래 키를 관리하도록 한다.

다대다 [N:N]

  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.

    • 보통 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.
  • 예를 들어, 회원들은 상품들을 주문하고 상품들은 회원들에 의해 주문된다.

    • 둘은 다대다 관계이고, 회원 테이블과 상품 테이블만으로는 이 관계를 표현할 수 없다.

    image

    • 중간에 연결 테이블을 추가해서 다대다 관계를 일대다, 다대일 관계로 풀어낼 수 있다.
  • 객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있다.

    • 두 객체 모두 컬렉션을 사용해서 상대 엔티티를 참조하면 된다.
    • @ManyToMany 를 사용하여 다대다 관계를 매핑할 수 있다.

다대다 : 단방향

@Entity
public class Member {
  @Id
  @Column(name="MEMBER_ID")
  private Long id;
  
  @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;
  
  ..
}
  • @ManyToMany@JoinTable 을 사용해서 연결 테이블을 바로 매핑했다.
    • MEMBER_PRODUCT 테이블에 해당하는 엔티티 없이 매핑을 완료할 수 있다.
  • @JoinTable
    • name : 연결 테이블을 지정
    • joinColumns : 현재 엔티티와 매핑할 조인 컬럼 정보를 지정
    • inverseJoinColumns : 반대 엔티티와 매핑할 조인 컬럼 정보를 지정
  • Member 인스턴스와 Product 인스턴스를 생성하여 연관관계를 참조 값으로 설정 후 DB에 저장할 수 있다.

    Product productA = new Product("productA");
    em.persist(productA);
      
    Member member1 = new Member("member1");
    member.getProducts().add(productA);  // 연관관계 설정
    em.persist(member1);
    
    • 위 코드를 실행하면 다음과 같이 3개의 SQL이 실행된다.

      INSERT INTO PRODUCT VALUES('productA', ...)
      INSERT INTO MEMBER VALUES('member1', ...)
      INSERT INTO MEMBER_PRODUCT...
      
  • Member 엔티티 조회 후 Product 참조를 통해서 객체 그래프를 탐색할 수 있다.

    Member member = em.find(Member.class, "member1");
    List<Product> products = member.getProducts();
    for(Product product : products) {
      System.out.println("product : " + product.getName());
    }
    
    • 위 코드를 실행하면 다음과 같은 SELECT SQL이 실행된다.

      SELECT * 
      FROM MEMBER_PRODUCT MP
      INNER JOIN PRODUCT P ON MP.PRODUCT_ID = P.PRODUCT_ID
      WHERE MP.MEMBER_ID = 'member1'
      
      • 즉, 연결 테이블인 MEMBER_PRODUCTPRODUCT 테이블을 조인해서 연관된 상품들을 조회한다.

다대다 : 양방향

@Entity
public class Product {
  @Id
  private String id;
  
  @ManyToMany(mappedBy="products")
  private List<Member> members;
  ...
}
  • 반대 엔티티인 Product 에도 @ManyToMany 를 사용하여 양방향 연관관계를 설정한다.

    • mappedBy 로 연관관계의 주인을 지정해준다.
  • 양방향 연관관계는 연관관계 편의 메소드를 추가하는 것이 편하므로 연관관계의 주인인 Member 엔티티에 연관관계 편의 메소드를 추가한다.

    public void addProduct(Product product) {
      ...
      products.add(product);
      product.getMember().add(this);
    }
    

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

  • @ManyToMany 를 사용하면 연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지는 편리함이 있다.

    • 하지만 비즈니스적으로 한계를 가지고 있다.
    • 예를 들어, 회원이 상품을 주문하면 연결 테이블에 단순히 회원 아이디와 주문 아이디만 저장하는 것이 아니라 주문 수량이나 주문 날짜 같은 추가 정보들도 저장해야 한다.
    • 이렇게 컬럼을 추가하면 더는 @ManyToMany 를 사용할 수 없다.
      • Member 엔티티나 Product 엔티티에 추가한 컬럼들을 매핑할 수 없기 때문이다.
  • 연결 테이블을 매핑하는 연결 엔티티를 만들고 이곳에 추가한 컬럼들을 매핑해야 한다.

    • 즉, 엔티티 간의 관계가 다대다 에서 일대다, 다대일 관계로 바뀌게 된다.

    image

@Entity
public class Member {
  @Id
  @Column(name="MEMBER_ID")
  private String id;
  
  @OneToMany(mappedBy="member")
  private List<MemberProduct> memberProducts;
}
  • 회원과 회원상품을 양방향 연관관계로 만들었다.
  • MEMBER_PRODUCT 테이블이 외래 키를 가지고 있으므로 MemberProduct 가 연관관계의 주인이다.
@Entity
@IdClass(MemberProductId.class)
public class MemberProduct {
  @Id
  @ManyToOne
  @JoinColumn(name="MEMBER_ID")
  private Member member;
  
  @Id
  @ManyToOne
  @JoinColumn(name="PRODUCT_ID")
  private Product product;
  
  private int orderAmount;
  private Date orderDate;
}
  • @IdClass : 복합 기본 키 매핑

    • MemberProduct 엔티티는 기본 키가 MEMBER_IDPRODUCT_ID 로 이루어진 복합 기본키다.

    • JPA에서 복합키를 사용하려면 별도의 식별자 클래스를 지정하면 된다.

      public class MemberProductId implements Serializable {
        private String member;
        private String product;
            
        @Override
        public boolean equals(Object o) {...}
            
        @Override
        public int hashCode() {...}
      }
      
      • Serializable 인터페이스를 구현해야 한다.
      • equalshashCode 메소드를 구현해야 한다.
      • 기본 생성자가 있어야 한다.
      • 식별자 클래스는 public 이어야 한다.

식별 관계 (Identifying Relationship)

  • MEMBER_PRODUCTMEMBERPRODUCT 의 기본 키를 받아서 자신의 기본 키 + 외래 키로 사용한다.
  • 부모 테이블의 기본 키를 받아서 자신의 기본 키 + 외래 키로 사용하는 것을 데이터베이스 용어로 식별관계 라고 한다.

  • 복합 키를 사용하는 엔티티를 조회하기 위해서는 식별자 클래스를 생성해서 find() 메서드의 parameter로 전달해주어야 한다.

    MemberProductId memberProductId = new MemberProductId();
    memberProductId.setMember("member1");
    memberProductId.setProduct("productA");
      
    MemberProduct memberProduct = em.find(MemberProduct.class, memberProductId);
    Member member = memberProduct.getMember();
    Product product = memberProduct.getProduct();
    int orderAmount = memberProduct.getOrderAmount();
    
    • 이처럼 복합 키를 사용하면 ORM 매핑에서 처리해야 할 일이 많아지고 복잡해진다.

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

  • 연결 테이블이 복합 기본 키를 사용하지 않고, 데이터베이스에서 자동으로 생성해주는 대리 키를 사용하도록 한다.

    • 간편하고 거의 영구히 쓸 수 있으며 비즈니스에 의존하지 않는다는 장점이 있다.
  • MEMBER_PRODUCT 라는 이름 대신 ORDER 라는 이름의 테이블로 새로운 기본 키를 부여하여 사용한다.

    image

    @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;
      private Date orderDate;
    }
    
    • 식별자 클래스를 따로 사용하지 않기 때문에 엔티티 클래스나 저장, 조회하는 코드가 간단해진다.

다대다 관계를 일대다, 다대일 관계로 풀어내기 위해 연결 테이블을 만들 때 식별자를 어떻게 구성할지 선택해야 한다.

  • 식별 관계 : 부모 테이블에서 받아온 식별자를 기본 키 + 외래 키로 사용한다.
  • 비식별 관계 : 받아온 식별자는 외래 키로만 사용하고 새로운 식별자를 추가한다.