Jpa-연관관계, 상속관계 매핑

4 분 소요

연관관계 매핑 기초

  • 객체는 참조를 사용해서 연관된 객체를 찾고 / 테이블은 외래키를 사용해서 연관된 테이블을 찾는다.
  • 연관관계 매핑의 핵심은 객체의 참조와 테이블의 외래 키를 매핑하는 것이다.

  • 핵심 개념
    • 단방향 vs 양방향
    • 다대일 vs 일대다 vs 일대일 vs 다대다
    • 양방향일 경우 연관관계의 주인
  • 참조를 통한 객체간의 연관관계는 언제나 단방향이다. (객체 그래프 탐색)
  • But 외래 키를 통한 테이블 간의 연관관계는 언제나 양방향이다.

  • 단방향
      public class Member {
        @ManyToOne
        @JoinColumn(name = "TEAM_ID")
        private Team team;
      }
    
  • 위 코드는 Member객체의 team필드와 MEMBER테이블의 TEAM_ID외래 키 컬럼을 매핑하는 코드이다.

  • 위와 같이 단방향으로 연관관계를 맺었을 때 엔티티를 조회하는 방법은 두 가지이다.
    1. 객체 그래프 탐색
        Member member = em.find(Member.class, "member1");
        Team team = member.getTeam();
        Sytem.out.println("팀 이름 : " + team.getName());
      
    2. JPQL사용
        String jpql = "select m from Member m join m.team t where t.name=:teamName";
        List<Member> resultList = em.createQuery(jpql, Member.class).setParameter("teamName", "팀1").getResultList();
        for(Member member : resultList) {
      Sytem.out.println("멤버 이름 : " + member.getName());
        }
      
  • 양방향
  • 위에는 Member → Team의 단방향이었지만 Member ↔ Team 양방향 매핑을 하려면 아래와 같이 각각의 엔티티에 필드를 추가해야한다. (Member는 그대로, Team만 추가)
      public class Member {
        @ManyToOne
        @JoinColumn(name = "TEAM_ID")
        private Team team;
      }
        
      public class Team {
        @OneToMany(mappedby = "team")
        private List<Member> members = new ArrayList<Member>();
      }
    
  • 이렇게 양방향이되면 기존의 단방향에서 반대방향으로 객체 그래프 탐색 기능만이 추가된다. (Member ← Team 으로의 객체 그래프 탐색)
  • 단방향으로만 연관관계가 설정되어도 Member.team필드로 외래 키 관리(등록, 수정, 삭제)는 충분히 가능하다.
  • 단방향으로 할지 양방향으로 할지를 결정하는건 비즈니스요구에 따라 다르다. 멤버에서 팀을 조회할 일은 많지만 팀에서 어떤 멤버가 있는지를 조회할 일이 별로 없으면 위의 단방향설계가 맞는 것이다.
  • 이렇게 양방향일 경우 항상 주인은 @ManyToOne이 있으며 외래 키를 관리(등록, 수정, 삭제)하는 객체이다. 그래서 @JoinColumn(name=”외래 키”)도 항상 따라온다.
  • 주인이 아닌 @OneToMany는 속성으로 mappedby를 설정하고 값은 주인객체의 연관관계의 필드문자열이다.
  • 보통 @OneToMany아래의 필드는 리스트나 컬렉션 형태로 된다.
    team1.getMembers().add(member1);  //무시
    member1.setTeam(team1) // 연관관계 설정 가능
  • 위 코드와 같이 외래 키/연관관계 등록은 주인의 입장에서만 가능하다.
  • 하지만 순수한 객체까지 고려해서 양방향 연관관계를 짜는 것이 안전하므로 주인인 Member객체의 setTeam()메소드만으로 한큐에 해결할 수 있게 아래와 같이 리팩토링한다.
      public void setTeam(Team team) {
        
          //team을 셋팅하기 전에 존재한다면 해당 팀의 멤버에서 지금 이 멤버를 제거
          if (this.team != null){
              this.team.getMembers().remove(this);
          }
        
          this.team = team;
        
          //팀에 멤버를 참조할 수 있도록 추가한다.
          if(!team.getMembers().contains(this)) {
              team.getMembers().add(this);
          }
      }
    

연관관계 매핑 심화

  • 일대일
  • 예시 : 회원은 하나의 사물함만 가지고 사물함도 하나의 회원에 의해서 사용될 때 (Member, Locker)

  • @OneToOne으로 어느 쪽에 외래 키를 넣을지 정해야한다.

  • 주 테이블에 외래 키 (Member에 외래 키)
    • 단방향
        public class Member {
       @OneToOne
       @JoinColumn(name = "LOCKER_ID")
       private Locker locker;
        }
      
    • 양방향
        public class Member {
       @OneToOne
       @JoinColumn(name = "LOCKER_ID")
       private Locker locker;
        }
        public class Locker {
       @OneToOne(mappedby = "locker")
       private Member member;
        }
      
    • 여기에서도 다대일/일대다 연관관계에서와 마찬가지로 일대일 양방향 연관관계면은 주인 엔티티에선 set메소드를 리팩토링해줘야한다.
    • 사실 양방향 연관관계에선 주인엔티티에서 리팩토링을 전부 해야한다. 다대일이든 다대다든 일대일이든
  • 다대다
  • 양방향 (단방향으로 하려면 Item엔티티에서 categories필드 없애면 됨)
  • 테이블은 3개, 엔티티는 2개로 풀어내기
      public class Member {
      @ManyToMany  //주인
      @JoinTable(name = "member_product", joinColumns = @JoinColumn(name = "member_id"), inverseJoinColumns = @JoinColumn(name = "product_id"))
      private List<Product> products = new ArrayList<Product>();
      }
        
      public class Product{
        @ManyToMany(mappedBy = "members")  //하인
        private List<Member> members = new ArrayList<Member>();
      }
    
    • 이렇게 하면 ToMany니까 필드는 리스트로 받아줘야한다.
    • 이러면 테이블은 MEMBER, PRODUCT, MEMBER_PRODUCT 이렇게 3개 생긴다.
    • MEMBER_PRODUCT는 다대다를 일대다, 다대일 관계로 풀어내기 위해 필요한 연결테이블일 뿐이다.

    • 위와 같이 @ManyToMany는 엔티티없이 새로운 테이블을 만들어내고 조인컬럼들을 컬럼으로 갖지만 그 외에 컬럼을 추가할 수가 없다는 문제점이 있다. 실무에선 연결 테이블에 추가로 컬럼을 관리해야할 상황이 많이 찾아오므로 위 방식은 권장하지 않는다.
    • 그래서 엔티티 3개, 테이블 3개로 다대일/일대다 방식으로 풀어야한다. 이 방법도 식별관계와 비식별관계로 나누어 구현할 수 있다. 엔티티(Member↔Order→Product)
  • 식별 관계 (Member↔MemberProduct→Product) - pk와 fk를 동시에 설정
    • 연결엔티티에 따로 pk를 두지않고 외래 키 두개를 동시에 pk로 설정해서 복합키로 새롭게 매핑한다. 그래서 연결엔티티엔 @Id가 두 개다.
    • @IdClass(MemberProductId.class)를 @Entity와 함께 둔다.
    • Serializable인터페이스를 구현하는 MemberProductId클래스를 따로 만들어야한다.
    • 이외에 더 복잡하지만 일단은 난 비식별관계로 사용할 예정이므로 깊게 공부 X
  • 비식별 관계 (Member↔Order→Product) - pk와 fk를 분리
    • 연결엔티티에 유일한 PK를 데이터베이스에서 자동으로 생성해주는 대리 키 Long을 사용한다. 외래 키는 평범하게 양 쪽의 각각 두 개를 사용한다.
    public class Member{
      @OneToMany(mappedby = "member")
      private List<Order> orders = new ArrayList<Order>();
    }
    
    public class Order{
      @Id
      @GeneratedValue
      private Long id;
    
      @ManyToOne
      @JoinColumn(name="MEMBER_ID") 
      private Member member;
      
      @ManyToOne
      @JoinColumn(name="PRODUCT_ID")
      private Product product;
    }
    
    public class Product{
      // order와 product는 양방향이 아니므로 연관관계 매핑없음
    }

상속 관계 매핑

  • 조인 전략
    • @Inheritance(strategy = InheritanceType.JOINED) → 부모에서
    • @DiscriminatorColumn(name = “DTYPE”) → 부모에서
    • @DiscriminatorValue(“M”) → 자식에서
    • Item 밑에 Movie, Book, Album 테이블이 상속되어있게 매핑한다.
    • Item의 pk인 item_id를 자식테이블에게 pk이자 fk로 물려준다. (이외에 name이나 price컬럼도 상속한다)
    • 상속받은 pk는 @PrimaryKeyJoinColumn(name=”book_id”)와 같이 컬럼명을 재정의할 수도 있다. (optional)
  • 단일 테이블 전략
    • @Inheritance(strategy = InheritanceType.SINGLE_TABLE) → 부모에서
    • 나머지 어노테이션은 조인전략과 같음.
    • 이 전략은 따로 자식테이블 3개를 만드는 것이 아닌 Item테이블 하나에 모든 컬럼들을 때려박고 어떤 아이템인지는 DTYPE구분컬럼으로 구분해서 해당 아이템의 컬럼만 NULL하지않게 관리한다. 따라서 그 밖의 아이템의 컬럼들은 NULL을 허용해야한다.
  • @MappedSuperclass
    • @Entity대신 이게 들어가서 테이블로 매핑은 되지않지만 해당 클래스의 필드들로 매핑정보만 상속받게끔할 때 이걸 쓴다. 주로 클래스는 public abstract class BaseEntity 이렇게 구현하고 보통 공통적으로 사용하는 날짜같은 필드 매핑정보를 넘겨준다.
  • 복합키와 식별 관계 매핑 (잘 안쓰임)
    • 식별 관계
    • 비식별 관계
    • @IdClass, @EmbeddedId (복합키 지원 어노테이션)
  • 조인 테이블 (잘 안쓰임)

  • 엔티티 하나에 여러 테이블 매핑 (잘 안쓰임)

카테고리:

업데이트:

댓글남기기