본문 바로가기

IT/데이터베이스

JPA 공부 - 5

반응형

2021/01/29 - [IT/데이터베이스] - JPA 공부 - 4

 

5. 연관관계 매핑 기초

  • 객체는 참조(주소)를 사용해서 관계를 맺고 테이블은 외래 키를 사용해서 관계를 맺는다.
  • 객체의 참조와 테이블의 외래 키를 매핑해보자
  • 방향
    • 예를 들어 회원과 팀이 있을 때 둘 중 한쪽만 참조하는 것을 단방향 관계라고 한다.
    • 반대로 양쪽 모두 서로 참조하는 것을 양방향 관계라 한다.
    • 방향은 객체관계에서만 존재하고 테이블은 항상 양방향이다.
  • 다중성
    • [다대일(N:1)], [일대다(1:N)], [일대일(1:1)], [다대다(N:M)] 다중성이 있다.
    • 회원과 팀 관계에서 여러 회원은 한 팀에 속하므로 회원과 팀은 다대일 관계다.
    • 반대로 한 팀에 여러 회원이 소속될 수 있으므로 팀과 회원은 일대다 관계다.
  • 연관관계의 주인
    • 객체를 양방향 연관관계로 만들면 연관 관계의 주인을 정해야 한다.

5.1. 단방향 연관관계

  • 제약조건

    • 회원과 팀이 있다.
    • 회원은 하나의 팀에만 소속될 수 있다.
    • 회원과 팀은 다대일 관계다
      다대일 연관관계 | 다대일(N:1), 단방향
  • 객체 연관관계

    • 회원 객체는 Member.team 필드로 팀 객체와 연관 관계를 맺는다.
    • 회원 객체와 팀 객체는 단방향 관계다. 회원은 팀을 알 수 있지만 팀은 회원 알 수 없다.
  • 테이블 연관관계

    • 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다.

    • 회원 테이블과 팀 테이블은 양방향 관계다

    • TEAM_ID 외래키를 통해 회원과 팀을 조인할 수 있고, 반대로 팀과 회원도 조인할 수 있다.

      SELECT *
        FROM MEMBER M
       JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
      
      SELECT *
        FROM TEAM T
        JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
  • 객체 연관관계와 테이블 연관관계의 가장 큰 차이

    • 참조를 통한 연관관계는 언제나 단방향이다.

    • 객체 간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다.

    • 정확히 이야기하면 이것은 양방향 관계가 아니라 단방향 관계 2개다.

      class A {
          B b;
      }
      
      class B {
          A a;
      }
  • 객체 연관관계 vs 테이블 연관관계 정리

    • 객체는 참조(주소)로 연관과계를 맺는다.
    • 테이블은 외래 키로 연관관계를 맺는다.
    • 연관된 데이터를 조회할때 객체는 참조를 사용하지만 테이블은 조인(JOIN)을 사용한다.

5.1.1 순수한 객체 연관관계

  • 아래는 JPA를 사용하지 않은 순수한 회원과 팀 클래스의 코드다.

    public class Member {
    
        private String id;
        private String username;
    
        private Team team; // 팀의 참조를 보관
    
        // getter, setter
    }
    
    
public class Team {
    private String id;
    private String name;

    // getter, setter
}
```
  • 회원을 팀에 포함시키는 예제는 아래와 같다.

    public static void main(String[] args) {
        Member member1 = new Member("member1", "회원1");
        Member member2 = new Member("member2", "회원2");
    
        Team team1 = new Team("team1", "팀1");
    
        member1.setTeam(team1);
        member2.setTeam(team1);
    
        Team findTeam = member1.getTeam();
    }
  • 객체는 객체 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라 한다.

5.1.2 테이블 연관관계

  • 데이터베이스 테이블의 회원과 팀의 관계를 표시하기 위한 DDL이다.

    CREATE TABLE MEMBER (
        MEMBER_ID VARCHAR(255) NOT NULL,
        TEAM_ID VARCHAR(255),
        USERNAME VARCHAR(255),
        PRIMARY KEY (MEMBER_ID)
    )
    
    CREATE TABLE TEAM (
        TEAM_ID VARCHAR(255) NOT NULL,
        NAME VARCHAR(255),
        PRIMARY KEY (TEAM_ID)
    )
    
    -- 회원 테이블의 TEAM_ID 에 외래키 제약조건 설정
    ALTER TABLE MEMBER ADD CONSTRAINT FK_MEMBER_TEAM
        FOREIGN KEY (TEAM_ID)
        REFERENCES TEAM
  • 회원을 팀에 소속시키고 검색하는 QUERY는 아래와 같다.

    -- 삽입
    INSERT INTO TEAM(TEAM_ID, NAME) VALUES('team1', '팀1');
    INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES('member1', 'team1', '회원1');
    INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES('member2', 'team2', '회원2');
    
    -- 조회
    SELECT T.*
      FROM MEMBER M
      JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
     WHERE M.MEMBER_ID = 'member1';

5.1.3 객체 관계 매핑

  • JPA를 통해 둘을 매핑해보자

    다대일 연관관계1 | 다대일(N:1), 단방향
  • 엔티티 예제 코드

    @Entity
    public class Member {
    
        @Id
        @Column(name = "MEMBER_ID")
        private String id;
        private String username;
    
        // 연관관계 매핑
        @ManyToOne
        @JoinColumn(name = "TEAM_ID")
        private Team team;
    
        public void setTeam(Team team) {
            this.team = team;
        }
    
        // getter, setter
    }
    
    @Entity
    public class Team {
    
        @Id
        @Column(name = "TEAM_ID")
        private String id;
    
        private String name;
    
        // getter, setter
    }
  • 객체 연관관계 : 회원 객체의 Member.team 필드 사용

  • 테이블 연관관계 : 회원 테이블의 MEMBER.TEAM_ID 외래 키 컬럼을 사용

  • Member.team과 MEMBER.TEAM_ID를 매핑하는 것이 연관관계 매핑이다.

  • @ManyToOne : 이름 그대로 다대일(N:1) 관계라는 매핑 정보다. 연관 관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.

  • @JoinColumn(name="TEAM_ID) : 조인 컬럼은 외래 키를 매핑할 때 사용한다. name 속성에는 매핑할 외래 키 이름을 지정한다.

5.1.4 @JoinColumn

@JoinColumn의 주요 속성

참고
@JoinColumn 생략
@JoinColumn을 생략하면 기본 전략을 사용한다.

@ManyToOne
private Team team;
  • 기본전략 : 필드명 + _ + 참조하는 테이블의 컬럼명
  • 예 : 필드명(team) + _(밑줄) + 참조하는 테이블의 컬럼명(TEAM_ID) = team_TEAM_ID 외래 키를 사용

5.1.5 @ManyToOne

@ManyToOne 속성

5.2. 연관관계 사용

5.2.1 저장

  • 아래는 연관관계를 매핑한 엔티티를 저장하는 예제이다.

    // 팀 저장
    Team team = new Team("team1", "팀1");
    em.persist(team);
    
    // 회원 1 저장
    Member member1 = new Member("member1", "회원1");
    member1.setTeam(team); // 연관관계 설정 member1 -> team1
    em.persist(member1);
    
    // 회원 2 저장
    Member member2 = new Member("member2", "회원2");
    member2.setTeam(team); // 연관관계 설정 member2 -> team1
    em.persist(member2);
  • 저장된 값을 보면 회원 테이블의 외래 키 값으로 참조한 팀의 식별자 값인 team1이 입력된 것을 확인할 수 있다.

5.2.2 조회

  • 연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지다.

    • 객체 그래프 탐색 (객체 연관관계를 사용한 조회)
    • 객체지향 쿼리 사용(JPQL)
  • 객체 그래프 탐색

    • member.getTeam()을 사용해서 member와 연관된 team 엔티티를 조회할 수 있다.
  • 객체지향 쿼리 사용

    • 회원을 대상으로 조회할 때 특정 팀에 속한 회원을 조회하려면 sql은 조인을 해서 검색 조건을 사용하면 된다.

    • 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){
        System.out.println("[query] member.username = " + member.getUsername());
      }
    • 팀과 관계를 가지고 있는 필드(m.team)를 통해서 Member와 Team을 조인했다.

    • :teamName과 같이 :로 시작하는 것은 파라미터 바인딩 문법이다.

    • 이때 실행되는 SQL은 아래와 같다.

      select
          member0_.MEMBER_ID as MEMBER_I1_0_,
          member0_.TEAM_ID as TEAM_ID3_0_,
          member0_.username as username2_0_
      from
          Member member0_
      inner join
          Team team1_
              on member0_.TEAM_ID=team1_.TEAM_ID
      where
          team1_.name=?

5.2.3 수정

  • 팀 1 소속이던 회원을 새로운 팀 2에 소속하도록 수정해보자

    private static void updateRelation(EntityManager em) {
      Team team2 = new Team("team2", "팀2");
      em.persist(team2);
    
      Member member = em.find(Member.class, "member1");
      member.setTeam(team2);
    }
  • 실행되는 sql은 아래와 같다.

    update
          Member
      set
          TEAM_ID=?,
          username=?
      where
          MEMBER_ID=?
  • 수정은 em.update() 같은 메서드가 없다.

  • 단순히 엔티티 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동한다.

  • 연관관계를 수정할 때도 참조하는 대상만 변경하면 나머지는 JPA가 자동으로 처리한다.

5.2.4 연관관계 제거

  • 연관된 엔틴티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다.

  • 그렇지 않으면 외래 키 제약조건으로 인해, 데이터베이스에서 오류가 발생한다.

    member1.setTeam(null);
    member2.setTeam(null);
    
    em.remove(team);

5.3 양방향 연관관계

  • 이번에는 반대방향에서 접근할 수 있도록 팀에서 회원으로 접근하는 관계를 추가하자.

  • 일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다.

    양방향 객체 연관관계
    • 회원 -> 팀 (Mebmer.team)
    • 팀 -> 회원 (Team.members)

5.3.1 양방향 연관관계 매핑

  • Team 엔티티에 양방향 관계를 설정하자

      @Entity
      public class Team {
    
          @Id
          @Column(name = "TEAM_ID")
          private String id;
    
          private String name;
    
          // 추가
          @OneToMany(mappedBy = "team")
          private List<Member> members = new ArrayList<>();
      }
  • 팀과 회원은 일대다 관계다. 따라서 팀 엔티티에 컬렉션인 List members를 추가했다.

  • 그리고 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용했다.

  • mappedBy 속성은 반대쪽 매핑의 필드 이름값으로 주면 된다.

5.3.2 일대다 컬렉션 조회

  • 객체 그래프 탐색을 통해 조회한 회원들을 출력하는 예제이다

      Team2 team = em.find(Team2.class, "team1");
      List<Member2> members = team.getMembers();
    
      for(Member2 member : members){
          System.out.println("member.username = " + member.getUsername());
      }

5.4 연관관계의 주인

  • 단순히 @OneToMany만 있으면 되지 mappedBy는 왜 필요한 것인가?
  • 엄밀히 말하면 객체는 양방향 연관관계라는 것은 없다. 서로 다른 단방향 연관관계 2개를 로직으로 묶은 것뿐이다.
  • 테이블은 외래 키 하나로 두 테이블 연관관계를 관리한다.
  • 엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외래 키를 관리하면 된다.
  • 양방향으로 관리하려면 두 곳에서 서로를 참조해야 하고, 객체의 참조는 둘인데 외래 키는 하나이기 때문에 둘 사이에 차이가 발생한다.
  • 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 하는데 이것을 연관관계의 주인이라 한다.

5.4.1 양방향 매핑의 규칙: 연관관계의 주인

  • 두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다.

  • 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제) 할 수 있다. 반면 주인이 아닌 쪽은 읽기만 할 수 있다.

  • 주인은 mappedBy 속성을 사용하지 않는다.

  • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.

  • 그렇다면 Member.team, Team.members 중 어떤 것을 연관관계의 주인으로 정해야 할까?

    둘 중 하나를 연관관계의 주인으로 선택해야 한다.
  • 회원 -> 팀(Member.team) 방향

      class Member {
          @ManyToOne
          @JoinColumn(name="TEAM_ID")
          private Team team;
          ...
      }
    
      class Team {
          @OneToMany
          private List<Member> members = new ArrayList<Member>();
      }
  • 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다.

  • 만약 Member.team을 주인으로 선택하게 되면 자기 테이블에 있는 외래 키를 관리하게 된다.

  • 하지만 Team.members를 주인으로 선택하면 물리적으로 전혀 다른 테이블의 외리 키를 관리해야 한다.

5.4.2 연관관계의 주인은 외래 키가 있는 곳

  • 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.
  • 주인이 아닌 엔티티에서는 mappedBy 속성을 사용해 주인이 아님을 설정한다.
  • 정리하면 주인만 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다. 주인이 아닌 반대편은 읽기만 가능하고 외래 키를 변경하지는 못한다.

5.5 양방향 연관관계 저장

  • 아래는 양방향 연관관계를 사용해서 팀1, 회원1, 회원2를 저장해 보는 예제이다.

      // 팀 저장
      Team team = new Team("team1", "팀1");
      em.persist(team);
    
      // 회원 1 저장
      Member member1 = new Member("member1", "회원1");
      member1.setTeam(team); // 연관관계 설정 member1 -> team1
      em.persist(member1);
    
      // 회원 2 저장
      Member member2 = new Member("member2", "회원2");
      member2.setTeam(team); // 연관관계 설정 member2 -> team1
      em.persist(member2);
  • 위 코드는 단방향 연관관계에서 살펴본 코드와 완전히 같다.

  • 양방향 연관관계에서 주인이 외래 키를 관리한다. 따라서 주인이 아닌 방향은 값을 설정하지 않아도 데이터베이스에 외래 키 값이 정상 입력된다.

      team1.getMembers().add(member1); // 무시 (연관관계의 주인이 아님)
      team1.getMembers().add(member2); // 무시 (연관관계의 주인이 아님)
  • 주인이 아닌 곳에 입력된 값은 외래 키에 영향을 주지 않는다.

5.6 양방향 연관관계의 주의점

  • 가장 대표적으로 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것

      public void testSaveNonOwner() {
    
          // 회원 1 저장
          Member member1 = new Member("member1", "회원1");
          em.persist(member1);
    
          // 회원 2 저장
          Member member2 = new Member("member2", "회원2");
          em.persist(member2);
    
          Team team1 = new Team("team1", "팀1");
          team1.getMembers().add(member1);
          team1.getMembers().add(member2);
    
          em.persist(team1);
      }
  • 위 코드 같은 경우 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문에 외래 키에 실제 null값이 입력된다.

5.6.1 순수한 객체까지 고려한 양방향 연관관계

  • 객체 관점에서 양쪽 방향에 모두 갑을 입력해주는 것이 가장 안전하다.
  • 순수한 객체 사용 시 심각한 문제가 발생할 수도 있다.
  • 순수한 객체를 사용하는 로직에서도 정상 동작을 위해 서로 연관관계를 만들어 주는 것이 실수를 줄일 수 있다.

5.6.2 연관관계 편의 메서드

  • 양방향을 신경 쓰다 보면 누락 가능성이 있다.

  • 아래처럼 메서드 리팩토링을 통한 방법이 있다.

      public class Member {
          private Team team;
    
          public void setTeam(Team team) {
              this.team = team;
              team.getmMembers.add(this);
          }
          ...
      }

5.6.3 연관관계 편의 메서드 작성시 주의사항

  • 편의 메서드 사용 시 주의해야 할 점이 있다. 아래 코드는 버그가 있는 코드이다.

      member1.setTeam(teamA);
      member1.setTeam(teamB);
    
      Mmeber findMember = teamA.getMember(); // member1이 아직 조회 된다.
  • temaB로 변경할대 teamA -> member1 관계를 제거하지 않았다.

  • 연관관계를 변경할 때는 기존 연관관계를 완전히 삭제하는 코드를 추가해 주어야 한다.

      public class Member {
          private Team team;
    
          public void setTeam(Team team) {
    
              if(team != null)
                  this.team.getMembers().remove(this);
    
              this.team = team;
              team.getmMembers.add(this);
          }
          ...
      }
반응형

'IT > 데이터베이스' 카테고리의 다른 글

JPA 공부 - 6  (0) 2021.02.25
JPA 공부 - 3  (0) 2021.02.25
JPA 공부 - 4  (0) 2021.01.29
JPA 공부 - 2  (0) 2021.01.21
JPA 공부 - 1  (0) 2021.01.18