6. 다양한 연관관계매핑


다대일 단방향 [N:1]

  • 가장 많이 사용되는 연관관계
  • 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]

  • 2
  • 실선이 연관관계의 주인(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]

  • 3
  • 팀 엔티티의 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]

  • 3
  • 일대다 단방향 매핑에서 역참조가 가능하도록 읽기 전용 단방향 매핑을 만들어야 함
  • // 팀 엔티티
      @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]

  • 외래키를 어디에 두냐에 따라 나뉘어짐
  • 주 테이블에 외래키
    • 외래키를 객체 참조와 비슷하게 사용할 수 있음
    • 주 테이블이 외래키를 가지고 있음
  • 대상 테이블에 외래키
    • 전통적인 데이터베이스 개발자들이 선호하는 방식
    • 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지 가능


  • 주 테이블에 외래키 + 단방향
    • 5
    •   // 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;
        }
      
  • 주 테이블에 외래키 + 양방향
    • 6
    • 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에서 지원하지도 않고 매핑할 수 있는 방법도 없음…
    • 7
  • 대상 테이블에 외래키 + 양방향
    • 8
    •   // 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개로 다대다 관계를 표현할 수 없음
  • 중간에 연결 테이블을 추가해서, 연결 테이블과 일대다, 다대일 관계를 유지해서 다대다 처럼 사용이 가능
  • 9
  • 그러나 객체를 사용할 땐 객체 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는 실무에서 사용하기 힘든데, 비즈니스 모델을 다대다로 적용하다 보면 연결 테이블이 단순 연결만 하고 끝나지 않음
    • 10
    • 이렇게 연결 테이블에 주문수량, 주문시간같은 부가적인 데이터가 생길 수 밖에 없음
    • 그러면 어떻게 하냐?
      • 연결 테이블을 엔티티로 승격, @ManyToOne, @OneToMany로 관계를 나눔
      • 11
      • 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;
        }
      





© 2020.02. by blupine