[JPA] Chapter 6. 다양한 연관관계 매핑

joyful·2021년 7월 27일
0

JPA

목록 보기
10/18

들어가기 앞서

이 글은 김영한 님의 저서 「자바 ORM 표준 JPA 프로그래밍」을 학습한 내용을 정리한 글입니다. 모든 출처는 해당 저서에 있습니다.


✅ 엔티티 연관관계 매핑 시 고려해야 할 것

  • 다중성

    종류애노테이션방향비고
    다대일@ManyToOne단방향, 양방향가장 많이 사용
    일대다@OneToMany단방향, 양방향가장 많이 사용
    일대일@OneToOne◾ 주 테이블 단방향, 양방향
    ◾ 대상 테이블 단방향, 양방향
    다대다@ManyToMany단방향, 양방향거의 사용 x
  • 참조방향 : 객체 관계에서 어느쪽 관계를 참조하는가

    • 단방향 : 한 쪽만 참조
    • 양방향 : 서로 참조
  • 연관관계의 주인 : 두 객체 연관관계 중 데이터베이스 외래 키를 관리하는 연관관계

    • 보통 외래 키를 가진 테이블과 매핑한 엔티티를 선택
    • 주인이 아닌 방향 → 외래 키 읽기만 가능
    • mappedBy 속성은 연관관계의 주인이 아닌 쪽에서 사용
    • 다중성에서의 연관관계 주인은 왼쪽
      ex) 다대일 양방향 → 다(N)

6.1 다대일

6.1.1 다대일 단방향 [N:1]

💻 회원 엔티티

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")  //Member.team 필드를 TEAM_ID 외래키와 매핑
    private Team team;
    
    //Getter, Setter ...
    ...
}

💻 팀 엔티티

@Entity
public class Team {

    @Id @GeneratedVaue
    @Column(name = "TEAM_ID")
    private String id;
    
    private String name;
    
    //Getter, Setter ...
    ...
}
  • 회원은 Member.team으로 팀 엔티티를 참조할 수 있지만 팀에는 회원을 참조하는 필드 x
  • Member.team 필드로 회원 테이블의 TEAM_ID 외래키 관리

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

💻 회원 엔티티

@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 tema) {
        this.team = team;
        
        //무한루프에 빠지지 않도록 체크
        if(!team.getMembers().contains(this)) {
            team.getMembers().add(this);
        }
    }
}

💻 팀 엔티티

@Entity
public class Team {

    @Id @GeneratedVaue
    @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);
        }
    }
    
    ...
}

✅ 양방향은 외래 키가 있는 쪽이 연관관계의 주인이다

  • 다(N)쪽인 MEMBER 테이블이 외래 키 가지고 있음
    Member.team이 연관관계의 주인
  • JPA는 외래 키 관리 시 연관관계의 주인만 사용
  • 주인이 아닌 Team.members조회를 위한 JPQL이나 객체 그래프 탐색 시 사용

✅ 양방향 연관관계는 항상 서로를 참조해야 한다

  • 어느 한 쪽만 참조 → 양방향 연관관계 성립 x
  • 연관관계 편의 메소드 ex) setTeam(), addMember()
    • 연관관계가 항상 서로 참조하게 하기 위하여 작성
    • 한 곳에만 작성하거나 양쪽 다 작성 가능
      → 양쪽다 작성 시 무한루프에 빠질 수 있으므로 주의 필요


6.2 일대다

일대다 관계는 엔티티를 하나 이상 참조 가능하므로 자바 컬렉션인 Collection, List, Set, Map 중에 하나를 사용해야 한다.


6.2.1 일대다 단방향 [1:N]

📊 일대다 단방향 다이어그램

  • 다(N) 쪽이 아닌 일(1) 쪽의 엔티티에만 참조 필드 존재
    → 반대쪽 테이블에 있는 외래 키 관리

💻 팀 엔티티

@Entity
public class Team {

    @Id @GenereatedValue
    @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>();
    
    //Getter, Setter ...
}

💻 회원 엔티티

@Entity
public class Member {

    @Id @GenereatedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    //Getter, Setter ...
}
  • JPA는 조인 테이블 전략을 기본으로 사용하여 매핑
    → 일대다 단방향 관계 매핑 시 @JoinColumn 명시 필수

✅ 단점

  • 매핑한 객체가 관리하는 외래 키가 다른 테이블에 존재
    → 연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야 함

💻 예제 - 코드

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();
}

💻 예제 - 결과

insert into Member (MEMBER_ID, username) values (null, ?)
insert into Member (MEMBER_ID, username) values (null, ?)
insert into Team (TEAM_ID, name) values (null, ?)
update Member set TEAM_ID=? where MEMBER_ID=?
update Member set TEAM_ID=? where MEMBER_ID=?
  • Member 엔티티 저장 시 MEMBER 테이블의 TEAM_ID 외래 키에 저장되는 값 x
    • Member 엔티티는 Team 엔티티를 인식하지 못 함
    • 연관관계에 대한 정보는 Team 엔티티의 members가 관리
  • Team 엔티티 저장 시 Team.members의 참조 값 확인하여 회원 테이블의 TEAM_ID 외래 키 업데이트

✅ 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자

  • 일대다 단방향 매핑 사용
    • 엔티티 매핑 테이블이 아닌 다른 테이블의 외래 키 관리
    • 성능 및 관리 면에서 비효율적
  • 다대일 양방향 매핑 사용
    • 본인 테이블의 외래 키 관리 → 효율적

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

  • 일대다 양방향은 존재하지 않음
    • 관계형 데이터베이스의 특성상 일대다, 다대일 관계는 항상 다(N) 쪽에 외래키 존재
      → 양방향 매핑에서 @OneToMany는 연관관계의 주인이 될 수 없음
    • 일대다 양방향 대신 다대일 양방향 매핑 사용
  • 굳이 사용하고 싶다면?
    → 일대다 단방향 매핑 반대편에 같은 외래 키를 사용하는 다대일 단방향 매핑읽기 전용으로 하나 추가하여 사용

💻 팀 엔티티

@Entity
public class Team {

    @Id @GenereatedValue
    @Column(name = "TEAM_ID")
    private Long id;
    
    private String name;
    
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<Member>();
    
    //Getter, Setter ...
}

💻 회원 엔티티

@Entity
public class Member {

    @Id @GenereatedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(  //다대일 단방향 매핑 추가
        name = "TEAM_ID",
        insertable = false, updatealbe = false  //읽기 전용
    )
    private Team team;
    
    //Getter, Setter ...
}
  • 진정한 일대다 양방향 매핑이 아닌 일대다 양방향처럼 보이도록 하는 방법
    → 일대다 단뱡향 매핑이 가지는 단점을 그대로 가짐
    ∴ 다대일 양방향 매핑 사용 권장


6.3 일대일 [1:1]

양쪽이 서로 하나의 관계만 가지는 경우

✅ 특징

  • 일대일 관계는 그 반대도 일대일 관계
  • 주 테이블이나 대상 테이블 상관 없이 외래 키 가질 수 있음
    → 누가 외래키를 가질 지 선택 필요

6.3.1 주 테이블에 외래 키

  • 주 테이블에 외래 키를 두고 대상 테이블 참조
  • 외래 키를 객체 참조와 비슷하게 사용 가능
    → 객체지향 개발자들이 선호
  • 주 테이블 확인만으로 대상 테이블과의 연관관계 파악 가능

✅ 단방향

  • LOCKER_ID 외래 키에 유니크 제약 조건 추가

💻 회원 엔티티

@Entity
public class Member {

    @Id @GenereatedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
    ...
}

💻 사물함 엔티티

@Entity
public class Locker {

    @Id @GenereatedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    
    private String name;
    ...
}

✅ 양방향

💻 회원 엔티티

@Entity
public class Member {

    @Id @GenereatedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
    ...
}

💻 사물함 엔티티

@Entity
public class Locker {

    @Id @GenereatedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    
    private String name;
    
    @OneToOne(mappedBy = "locker")  //연관관계의 주인이 아님을 설정
    private Member member;
    ...
}
  • MEMBER 테이블이 외래 키 소유
    Member 엔티티의 Member.locker가 연관관계의 주인

6.3.2 대상 테이블에 외래 키

✅ 단방향

  • JPA에서 일대일 관계 중 대상 테이블에 외래 키가 있는 단방향 관계 지원 x
  • 매핑 방법 허용 x
  • 해결 방법
    • 단방향 관계를 Locker에서 Member 방향으로 수정
    • 양방향 관계로 만들고 Locker를 연관관계의 주인으로 설정

✅ 양방향

  • 일대일 매핑에서 대상 테이블에 외래 키를 두고 싶을 경우 사용

💻 회원 엔티티

@Entity
public class Member {

    @Id @GenereatedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @OneToOne(mappedBy = "member")
    private Locker locker;
    ...
}

💻 사물함 엔티티

@Entity
public class Locker {

    @Id @GenereatedValue
    @Column(name = "LOCKER_ID")
    private Long id;
    
    private String name;
    
    @OneToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
    ...
}
  • 대상 엔티티인 Locker를 연관관계의 주인으로 설정
    LOCKER 테이블의 외래 키 관리

💡 주의

  • 프록시를 사용할 때 일대일 관계는 지연 로딩으로 설정해도 즉시 로딩 된다.
    ex) Member.locker 지연 로딩 설정 → 즉시 로딩
  • 프록시의 한계로 인해 발생하는 문제이다.
  • bytecode instrumentation을 사용하여 해결할 수 있다.


6.4 다대다 [N:N]

  • 관계형 데이터베이스는 정규화 된 테이블 2개로 다대다 관계 표현 x
    → 연결 테이블 사용하여 다대다 관계 표현
  • 객체는 @ManyToMany를 사용하여 다대다 관계 매핑 가능

6.4.1 다대다: 단방향

💻 회원 엔티티

@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>();
    ...
}
  • @JoinTable 속성

    속성설명
    name연결 테이블 지정
    joinColumns현재 방향 엔티티와 매핑할 조인 컬럼 정보 지정
    inverseJoinColumns반대 방향 엔티티와 매핑할 조인 컬럼 정보 지정

💻 저장

public void save() {

    Product productA = new Product();
    productA.setId("productA");
    productA.setName("상품A");
    em.persist(productA);
    
    Member member1 = new Member();
    member1.setId("member1");
    member1.setUsername("회원1");
    member1.getProducts().add(productA);  //연관관계 설정
    em.persist(member1);
}
// 코드 실행
INSERT INTO PRODUCT ...
INSERT INTO MEMBER ...
INSERT INTO MEMBER_PRODUCT ...

💻 탐색

public void find() {

    Member member = em.find(Member.class, "memeber1");
    List<Product> products = member.getProducts();  //객체 그래프 탐색
    for (Product product : products) {
        System.out.println("product.name = " + product.getName());
    }
}
// 코드 실행
SELECT * FROM MEMBER_PRODUCT MP
INNER JOIN PRODUCT P ON MP.PRODUCT_ID=P.PRODUCT_ID
WHERE MP.MEMBER_ID=?
  • 연결 테이블인 MEMBER_PRODUCT와 상품 테이블을 조인하여 연관 상품 조회

6.4.2 다대다: 양방향

💻 역방향 추가

@Entity
public class Product {

    @Id
    private String id;
    
    @ManyToMany(mappedBy = "products")  //역방향 추가
    private List<Member> members;
}
  • 역방향 다대다 매핑 → @ManyToMany 사용
  • 양쪽 중 원하는 곳에 mappedBy로 연관관계 주인 지정
    mappedBy없는 곳연관관계의 주인

💻 연관관계 설정 1

member.getProducts().add(product);
product.getMembers().add(member);

💻 회원 엔티티에 연관관계 편의 메소드 추가

@Entity
pulbic class Member {

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

💻 연관관계 설정 2

member.addProduct(product);

💻 역방향 탐색

public void findInverse() {

    Product product = em.find(Product.class, "productA");
    List<Member> members = product.getMembers();
    for(Member member : members) {
        System.out.println("member = " + member.getUsername());
    }
}

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

@ManyToMany를 사용한 다대다 매핑은 실무에서 사용하기에는 한계가 존재한다.
예를 들어, 회원이 상품을 주문하면 연결 테이블에 주문한 회원 아이디와 상품 아이디 뿐만 아니라 주문 수량이나 주문 날짜와 같은 컬럼이 더 필요하다.

📊 연결 테이블에 필드 추가

연결 테이블에 주문 수량(ORDERAMOUNT)과 주문 날짜(ORDERDATE) 컬럼을 추가하면 더 이상 @ManyToMany를 사용할 수 없다. 왜냐하면, 주문 엔티티나 상품 엔티티에는 추가한 컬럼들을 매핑할 수 없기 때문이다.

📊 연결 엔티티 생성

연결 테이블을 매핑하는 연결 엔티티를 만들어 추가한 컬럼들을 매핑해야 한다. 또한 엔티티 간의 관계도 일대다, 다대일 관계로 풀어야 한다.

💻 회원 엔티티

@Entity
public class Member {

    @Id @Column(name = "MEMBER_ID")
    private String id;
    
    //역방향
    @OneToMany(mappedBy = "member")
    private List<MemberProduct> memberProduct;
    
    ...
}
  • 회원상품 엔티티 쪽이 외래 키를 가지고 있으므로 연관관계의 주인
    → 연관관계의 주인이 아닌 회원의 Member.memberProductsmappedBy 사용

💻 상품 엔티티

@Entity
public class Product {

    @Id @Column(name = "PRODUCT_ID")
    private String id;
    
    private String name;
    
    ...
}
  • 회원상품 엔티티로 객체 그래프 탐색 기능이 필요하지 않다고 판단
    → 연관관계 생성 x

💻 회원상품 엔티티

@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와 연결
    
    ...
}
  • 기본 키 : MEMBER_ID, PRODUCT_ID → 복합 기본 키

💻 회원상품 식별자 클래스

public class MemberProductId implements Serializable {

    private String member;  //MemberProduct.member와 연결
    private String product; //MemberProduct.product와 연결
    
    //hashCode and equals
    
    @Override
    public boolean equals(Object o) {...}
    
    @Override
    public int hashCode() {...}
}

✅ 복합 기본 키

  • JPA에서 복합 키를 사용하려면 별도의 식별자 클래스를 생성해야 함
  • 엔티티에 @IdClass를 사용하여 식별자 클래스 지정
  • 식별자 클래스의 특징
    • 복합 키는 별도의 식별자 클래스로 만들어야 한다.
    • Serializable을 구현해야 한다.
    • equalshashCode 메소드를 구현해야 한다.

      💡 자바 IDE에 대부분 자동 생성 기능 존재

    • 기본 생성자가 있어야 한다.
    • 식별자 클래스는 public이어야 한다.
    • @IdClass 외에 @EmbeddedId도 사용 가능 하다.

✅ 식별 관계(Identifying Relationship)

부모 테이블의 기본 키를 받아 자신의 기본 키 + 외래 키로 사용하는 것

  • 예시) 회원상품(MemberProduct)
    • 회원의 기본 키와 상품의 기본 키를 자신의 기본 키 + 외래 키로 사용
    • MemberProductId 식별자 클래스로 두 기본 키를 묶어 복합 기본 키로 사용

💻 저장

public void save() {

    //회원 저장
    Member member1 = new Member();
    member1.setId("member1");
    member1.setUsername("회원1");
    em.persist(member1);
    
    //상품 저장
    Product productA = new Product();
    productA.setId("productA");
    productA.setName("상품1");
    em.persist(productA);
    
    //회원상품 저장
    MemberProduct memberProduct = new MemberProduct();
    memberProduct.setMember(member1);   //주문 회원 - 연관관계 설정
    memberProduct.setProduct(productA); //주문 상품 - 연관관계 설정
    memberProduct.setOrderAmount(2);    //주문 수량
    
    em.persist(memberProduct);
}
  • 회원상품 엔티티는 데이터베이스에 저장될 때 연관된 회원의 식별자와 상품의 식별자를 가져와 자신의 기본 키 값으로 사용한다.

💻 조회

public void find() {

    //기본 키 값 생성
    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();
    
    System.out.println("member = " + member.getUsername());
    System.out.println("product = " + product.getName());
    System.out.println("orderAmount = " + memberProduct.getOrderAmount());
}
  • 복합 키는 항상 식별자 클래스를 만들어야 함
  • em.find(MemberProduct.class, memberProductId)
    → 생성한 식별자 클래스로 엔티티 조회

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

  • 추천하는 기본 키 생성 전략
    → 데이터베이스에서 자동으로 생성해주는 대리 키를 Long 값으로 사용
    • 장점
      • 간편함
      • 거의 영구적인 사용 가능
      • 비즈니스에 의존하지 않음
      • ORM 매핑 시 복합 키 생성 필요 x

📊 연결 테이블 - 주문(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;
    ...
}

💻 저장

public void save() {

    //회원 저장
    Member member1 = new Member();
    member1.setId("member1");
    member1.setUsername("회원1");
    em.persist(member1);
    
    //상품 저장
    Product productA = new Product();
    productA.setId("productA");
    productA.setName("상품1");
    em.persist(productA);
    
    //주문 저장
    Order order = new Order();
    order.setMember(member1);   //주문 회원 - 연관관계 설정
    order.setProduct(productA); //주문 상품 - 연관관계 설정
    order.setOrderAmount(2);    //주문 수량
    em.persist(order);
}

💻 조회

public void find() {

    Long orderId = 1L;
    Order order = em.find(Order.class, orderId);
    
    Member member = order.getMember();
    Product product = order.getProduct();
    
    System.out.println("member = " + member.getUsername());
    System.out.println("product = " + product.getName());
    System.out.println("orderAmount = " + order.getOrderAmount());
}

6.4.5 다대다 연관관계 정리

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

  • 식별 관계 : 받아온 식별자를 기본 키 + 외래 키로 사용
  • 비식별 관계(권장) : 받아온 식별자는 외래 키로만 사용하고 새로운 식별자 추가
    → 복합 키를 위한 식별자 클래스를 만들지 않아도 되므로 단순하고 편리한 ORM 매핑 가능
profile
기쁘게 코딩하고 싶은 백엔드 개발자

0개의 댓글