JPA 스터디 (5장)

54420 단어 JPAstudyJPA

5장 연관관계 매핑 기초

  • 목표
    • 객체의 참조와 테이블의 외래키를 매핑 하는 것
  • 5장의 핵심 키워드
    • 방향
      • 단방향, 양방향 이 있다.
      • ex) 회원, 팀 존재 시 회원→팀 또는 팀→회원 한쪽만 참조하는 것을 단방향 관계라 함
      • ex)회원 ↔ 팀 일 경우 양방향 관계라 함
      • 방향은 객체 관계에만 존재, 테이블 관계는 항상 양방향
    • 다중성
      • 다대일, 일대다, 다대다 의 다중성이 있다.
    • 연관관계의 주인
      • 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.

5.1 단방향 연관관계

  • 객체 연관관계와 테이블 연관관계의 가장 큰 차이

    • 객체 연관관계는 참조를 통해 연관관계를 맺기에 언제나 단방향이다.
    • 양쪽에서 서로 참조하게 끔 한다는 것은 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
    • 반면, 테이블은 외래 키 하나로 양방향으로 조인 할 수 있다.
  • 객체 연관관계 vs 테이블 연관관계 정리

    • 객체는 참조(주소)로 연관관계를 맺는다.
    • 테이블은 외래 키로 연관관계를 맺는다.
  • 순수한 객체 연관관계

    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();
    }
  • 테이블 연관관계

    • 데이터베이스의 경우 외래 키를 사용 해 연관관계를 탐색 할 수 있다. [조인]
  • 객체 관계 매핑

    @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 ..
    }
  • @ManyToOne

    • 다대일(N:1) 관계라는 매핑 정보, 회원과 팀은 다대일 관계이다. 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.
  • @JoinColumn(name="TEAM_ID")

    • 조인 컬럼은 외래 키를 매핑 할 때 사용 한다. 이 어노테이션은 생략 할 수 있다.
    • 생략 시 기본 전략을 사용 한다.
    • 기본 전략 : 필드명+"_(언더바)"+참조하는 테이블의 컬럼명
      • ex) team_TEAM_ID 외래 키를 사용 한다.

5.2 연관관계 사용

  • 저장

    
    //JPA 에서 엔티티를 저장 할때 연관된 모든 엔티티는 영속 상태여야 한다!
    public void testSave() {
    	
    	//팀1 저장
    	Team team1 = new Team("team1", "팀1");
    	em.persist(team1);
    
    	//회원1 저장
    	Member member1 = new Member("member1", "회원1");
    	member1.setTeam(team1); //연관관계 설정 member1 -> team1
    	em.persist(member1);
    	
    	//회원2 저장
    	Member member2 = new Member("member2", "회원2");
    	member2.setTeam(team1); //연관관계 설정 member2 -> team1
    	em.persist(member2);
    
    }
    
    //결과
    INSERT INTO TEAM (TEAM_ID, NAME) VALUES ('team1' , '팀1')
    INSERT INTO MEMBER (MEMBER_ID, NAME, TEAM_ID) VALUES('member1', '회원1','team1')
    INSERT INTO MEMBER (MEMBER_ID, NAME, TEAM_ID) VALUES('member2', '회원2','team1')
  • 조회

    • 객체 그래프 탐색 (객체 연관관계를 사용한 조회)

    • 객체지향 쿼리 사용 (JPQL)

      private static void queryLogicJoin(EntityManager em) {
      	String jpq1 = "select m from Member m join m.team t where " +
      								"t.name=:teamName"; //:으로 시작-> 파라미터 바인딩 받는 문법
      
      	List<Member> resultList = em.createQuery(jpq1, Member.class)
      			.setParameter("teamName","팀1")
      			.getResultList();
      
      	for (Member m : resultList) {
      		System.out.println("[query] m.username=" +
      			m.getUsername());
      	}
      		
      }
      
      //결과 : [query] m.username=회원1
      //결과 : [query] m.username=회원2
  • 수정

    private static void updateRelation(EntityManager em) {
    	//새로운 팀2
    	Team team2 = new Team("team2", "팀2");
    	em.persist(team2);
    
    	//회원1에 새로운 팀2 설정
    	Member member = em.find(Member.class , "member1");
    	member.setTeam(team2);
    }
    
    //실행되는 수정 SQL
    UPDATE MEMBER
    SET
    	TEAM_ID='team2', ...
    WHERE
    	ID='member1'
    
    //트랜잭션 커밋시 플러시가 일어나면서 변경 감지 기능 작동!
    //참조 대상만 변경하면 나머지는 JPA가 자동을 처리 함!
  • 연관관계 제거

    private static void deleteRelation(EntityManager em) {
    	Member member1 = em.find(Member.class, "member1");
    	member1.setTeam(null); //연관관계 제거
    }
    
    //실행 쿼리
    UPDATE MEMBER
    SET
    	TEAM_ID=null, ...
    WHERE
    	ID='member1'
    
  • 연관된 엔티티 삭제

    • 연관관계를 먼저 끊은 뒤 삭제 해야 한다. 그렇지 않으면 외래 키 제약조건으로 인해 DB 오류 발생

      member1.setTeam(null) //회원1 연관관계 제거
      member2.setTeam(null) //회원2 연관관계 제거
      em.remove(team); //팀 제거 

5.3 양방향 연관관계

  • 양방향 연관관계 매핑

    @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;
    	
    	@OneToMany(mappedBy = "team") //연관관계의 주인인 Member.team
    	private List<Member> members = new ArrayList<Member>();
    
    	//Getter, Setter ...
    }
    
    [테이블 연관관계]
    MEMBER 와 TEAM 테이블은 N:1 관계
    
    MEMBER              
    MEMBER_ID (PK)
    TEAM_ID (FK)
    USERNAME
    
    TEAM
    TEAM_ID(PK)
    NAME
  • 일대다 컬렉션 조회

    public void biDirection() {
    	
    	Team team = em.find(Team.class, "team1");
    	List<Member> members = team.getMembers(); //(팀->회원) //객체 그래프 탐색
    	
    	for(Member m : members) {
    		System.out.println("m.username="+m.getUsername());
    	}
    
    }
    
    //결과
    //m.username=회원1
    //m.username=회원2

5.4 연관관계의 주인

  • @OneToMany 만 있으면 되지 mappedBy는 왜 필요한가?
    • 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리 한다.
    • 객체 연관관계로 양방향을 구성 시 단방향 2개를 구성 한 것이 된다.
    • 즉, 엔티티를 양방향 연관관계로 설정 시 객체의 참조는 둘인데 외래 키는 하나 다.
    • 따라서 두 객체 간 차이가 생기는 것이므로 두 객체 연관관계 중 하나를 정해 테이블의 외래 키를 관리 해야 하는데 이를 연관관계의 주인 이라 한다.
  • 양방향 매핑의 규칙
    • 두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다. 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록,수정,삭제) 할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 가능!(master-slave)
    • 어떤 연관관계를 주인으로 정할지 mappedBy 속성을 사용 하게 된다.
    • 주인(master)은 mappedBy 속성을 사용하지 않음
    • 주인이 아닐 시 (slave) mappedBy 속성을 사용 해 속성의 값으로 연관관계의 주인을 지정 해야 한다.
    • 연관관계의 주인을 정한다는건 사실 외래 키 관리자를 선택 하는 것이다.
    • 예시 (5.3에서 설정한 Member 와 Team)
      • 회원 엔티티에 있는 Member.team 을 주인으로 선택 시 자기 테이블에 있는 외래 키를 관리 하면 된다.
      • 팀 엔티티에 있는 Team.members 를 주인으로 선택 시 물리적으로 다른 테이블의 외래 키를 관리 해야 한다. (Team 엔티티는 TEAM 테이블에 매핑되어 있는데, 관리 해야 할 외래 키는 MEMBER 테이블에 있기 때문)
  • 연관관계의 주인은 외래 키가 있는 곳
    • 연관관계의 주인은 외래 키가 있는 곳으로 정해야 한다.
    • 5.3에서 설정된 mappedBy 값으로 사용 된 team 은 연관관계의 주인 (master) 엔티티의 team 필드를 말한다.
    • 데이터베이스 테이블에서는 다대일,일대다 관계일 경우 항상 다 쪽이 외래 키를 가진다.
      • @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정 할 수 없다. (mappedBy속성 없음)

5.5 양방향 연관관계 저장

//5.2장에서 봤던 testSave()
public void testSave() {
	
	//팀1 저장
	Team team1 = new Team("team1", "팀1");
	em.persist(team1);

	//회원1 저장
	Member member1 = new Member("member1", "회원1");
	member1.setTeam(team1); //연관관계 설정 member1 -> team1 
	em.persist(member1);
	
	//회원2 저장
	Member member2 = new Member("member2", "회원2");
	member2.setTeam(team1); //연관관계 설정 member2 -> team1
	em.persist(member2);

}
-----------------------------------------
//SELECT * FROM MEMBER; 결과
MEMBER_ID | USERNAME | TEAM_ID
member1 | 회원1 | team1
member2 | 회원2 | team1
-----------------------------------------
team1.getMembers().add(member1) // 무시 됨 (team1은 연관관계의 주인이 아니다.)
team1.getMembers().add(member2) // 무시 됨 (team1은 연관관계의 주인이 아니다.)
-----------------------------------------
member1.setTeam(team1); //위 내용 대로 연관관계 설정 됨(Member가 연관관계의 주인)
member2.setTeam(team1); //위 내용 대로 연관관계 설정 됨(Member가 연관관계의 주인)

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

  • 흔한 실수

    • 연관관계의 주인에 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력 하는 실수 (DB 에 외래 키 값이 정상적으로 저장되지 않을 시 이를 먼저 의심해보자)

    • 연관관계 주인이 아닌 곳에만 값을 설정 시 어떻게 될까?

      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); 
      
      }
      
      //SELECT * FROM MEMBER; 결과
      
      MEMBER_ID | USERNAME | TEAM_ID
      member1 | 회원1 | null
      member2 | 회원2 | null
    • 연관관계의 주인이 아닌 Team.members에만 값을 지정했기 때문에 TEAM_ID 에는 null 값이 입력됨

    • 강조: 연관관계의 주인만이 외래 키의 값을 변경 할 수 있다.

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

    • 객체 관점에서 양쪽방향에 모두 값을 입력 해 주는 것이 가장 안전하다!

    • 양쪽 모두 값을 입력하지 않을 시 JPA를 사용하지 않는 순수 객체에서 심각한 문제가 발생 할 수 있음

    • 예시

      public void test순수한객체_양방향() {
      	
      	//팀1
      	Team team1 = new Team("team1", "팀1");
      	Member member1 = new Member("member1", "회원1");
      	Member member2 = new Member("member2", "회원2");
      
      	member1.setTeam(team1) //연관관계 설정  member1->team1
      	member2.setTeam(team1) //연관관계 설정  member2->team1
      
      	List<Member> members = team1.getMembers();
      	System.out.println("members.size = " + members.size());
      }
      //결과 : members.size = 0
      • 양방향은 양쪽 다 관계를 설정해야 한다. 회원→팀을 설정하면 다음 코드 처럼 반대방향인 팀→회원도 설정 해 줘야 한다.
        • team1.getMembers().add(member1) //팀→회원
    • 양쪽 모두 관계를 설정한 예시

      public void test순수한객체_양방향() {
      
      	//팀1
      	Team team1 = new Team("team1", "팀1");
      	Member member1 = new Member("member1", "회원1");
      	Member member2 = new Member("member2", "회원2");
      	
      	member1.setTeam(team1) //연관관계 설정  member1->team1
      	team1.getMembers().add(member1) //연관관계 설정 team1->member1
      
      	member2.setTeam(team1) //연관관계 설정  member2->team1
      	team1.getMembers().add(member2) //연관관계 설정 team1->member2
      
      	List<Member> members = team1.getMembers();
      	System.out.println("members.size = " + members.size());
      	
      }
      //결과 : member.size = 2
  • 결론 : 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주도록 하자

  • 연관관계 편의 메소드

    • 위에 설정한 두 관계설정 간 실수로 하나만 호출 하여 양방향이 깨질 수 있다.

    • 따라서 양방향 관계에서 두 코드는 하나인 것처럼 사용하는 것이 안전하다.

      public class Member {
      	private Team team;
      
      	public void setTeam(Team team) {
      		this.team = team;
      		team.getMembers().add(this);
      	}
      	...
      }
  • 연관관계 편의 메소드 작성 시 주의사항

    member1.setTeam(teamA);
    member1.setTeam(teamB);
    Member findMember = teamA.getMember(); //member1이 여전히 조회된다. 버그!
    • member1.setTeam(teamA)호출 한 직후 객체 연관관계

      member1 <-------> teamA
      		   teamB
      
      [삭제되지 않은 관계 1]
    • member1.setTeam(teamB)을 호출 한 직후 객체 연관관계

      member1 <---------- teamA
         ^
         |-------------->  teamB
      
      [삭제되지 않은 관계 2]
    • teamB  로 변경 할 때 teamA → member1의 관계를 제거하지 않았기 떄문에 연관관계 변경 시 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가 해야 한다!

      public void setTeam(Team team) {
      	
      	//기존 팀과 관계를 제거
      	if(this.team !=null) {
      		this.team.getMembers().remove(this);
      	}
      	this.team = team;
      	team.getMembers().add(this);
      }
    • 정리하자면 객체에서 양방향 연관관계를 사용하려면 로직을 견고하게 작성 해야 한다!

  • 참고

    • [삭제되지 않은 관계 2] 에서 teamA → member1 관계가 제거되지 않아도 데이터베이스 외래 키를 변경 하는데는 문제가 없다. 왜냐? teamA→member1 관계를 설정한 team.Members는 연관관계의 주인(master) 가 아니기 때문
    • 연관관계의 주인인 Member.team의 참조를 member1→teamB로 변경한 것이므로 데이터베이스에 외래 키는 teamB를 참조하도록 정상 반영 된다.
    • 문제는 관계를 변경 한 뒤 영속성 컨텍스트가 아직 살아있는 상태에서 teamA의 getMembers()를 호출하면 member1이 반환된다는 점이다. 따라서 변경된 연관관계는 위 내용처럼 관계를 제거→ 안전!

5.7 정리

  • 결국 양방향 매핑은 단방향 매핑과 비교해서 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것 뿐이다.
  • 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료 되었다.
  • 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가 된다.
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향 모두 관리 해야 한다.
  • 연관관계의 마스터를 정하는 기준
    • 비즈니스 로직 상 중요하다고 연관관계의 마스터로 선택하면 안된다!
    • 단순히 외래 키 관리자 정도의 의미만 부여 할 것
  • 앙뱡향 매핑 시 무한루프에 빠지지 않도록 조심!

실전예제 | 2. 연관관계 매핑 시작

좋은 웹페이지 즐겨찾기