-
#JPA-10 연관관계 매핑 기초JPA 2021. 2. 17. 23:01
[출처] 인프런 김영한 강사님 -자바 ORM 표준 JPA 프로그래밍 기본
1. 예제 시나리오
• 회원과 팀이 있다.
• 회원은 하나의 팀에만 소속될 수 있다.
• 회원과 팀은 다대일 관계다. N:1ERD Member Class
package jpabook.jpashop.domain; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity public class Member { @Id @GeneratedValue//생략하면 오토 @Column(name = "MEMBER_ID") private Long id; private String name; private String city; private String street; private String zipcode; //getter는 만들 필요가 있지만 setter는 생각을 한번 해봐야한다. //setter를 쓰면 아무대서나 set을 할수잇으니 코드 추척하기 어렵고, 유지보수 성이 떨어진다. //가급적이면 생성자에서 값을 세팅하고 setter의 사용을 최소화 해야한다. public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getStreet() { return street; } public void setStreet(String street) { this.street = street; } public String getZipcode() { return zipcode; } public void setZipcode(String zipcode) { this.zipcode = zipcode; } }
Team class
package hello.jpa; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private long id; private String name; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
JpaMain - 테이블 지향적 일때
package hello.jpa; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.Persistence; import java.util.List; public class JpaMain { public static void main(String[] args) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello"); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); try{ Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setUsername("member1"); member.setTeamId(team.getId()); //이게 애매하다. 외래키 식별자를 직접 다루는 것이다. em.persist(member); Member findMember = em.find(Member.class, member.getId()); Long findTeamId = findMember.getTeamId(); Team findTeam = em.find(Team.class, findTeamId); //이런식으로 계속 JPA한태 물어봐야한다. 왜냐하면 연관관계가 없기때문이다. 지금 코드는 객체지향적이 아니기때문이다. tx.commit(); } catch (Exception e){ tx.rollback(); }finally { em.close(); } emf.close(); } }
객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.
• 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
• 객체는 참조를 사용해서 연관된 객체를 찾는다.
• 테이블과 객체 사이에는 이런 큰 간격이 있다.
2. 단방향 연관관계
객체 지향 모델링 (객체 연관관계 사용)
객체 지향 모델링 • private Team team : 그냥 이대로 쓰면 error가 난다. jpa한태 말해줘야함 이둘의 관계가 1:N인지 N:1인지
• (@ManyToMany, @ManyToOne, @OneToMany, @OneToOne ) : 맴버입장에서는 하나의 팀에 여러 맴버가 소속되기 때문에 맴버가 N 팀 1
@ManyToOne(fetch = FetchType.EAGER) //member랑 team이랑 쿼리가 분리되서 나간다.
• team reference랑 테이블 연관관계의 team_id랑 join해야함.
• TEAM_ID 빨간줄 : intellj에서 name매핑에 빨간줄이 뜨는 것은 DB에 실제로 값이 있어야 찍히는거라서 무시해도된다.
• 맴버입장에서는 하나의 팀에 여러 맴버가 소속되기 때문에 맴버가 N 팀 1
• 객체 지향 모델링할때는 2개를 설정해야한다. 관계가 뭔지?,이 관계를 할때 조인하는 컬럼은 뭐야?
JpaMain - 객체 지향 모델링 (객체 연관관계 사용)
package hello.jpa; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.Persistence; import java.util.List; public class JpaMain { public static void main(String[] args) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello"); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); try{ Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setUsername("member1"); member.setTeam(team); //이러면 JPA가 알아서 TEAM에서 PK 값을 꺼내서 INSERT할때 FORGINE KEY 값을 사용한다. em.persist(member); em.flush(); em.clear(); //이렇게 하게 되면 캐시값을 초기화하고 다시 조회해서 가져온다. //이렇게 해야 깔끔하게 DB에서 값을 가져온다. Member findMember = em.find(Member.class, member.getId()) Team findTeam = member.getTeam(); //조회시 //Team 을 바꿀 경우 Team newTeam = em.find(Team.class, 100L); findMember.setTeam(newTeam); tx.commit(); } catch (Exception e){ tx.rollback(); }finally { em.close(); } emf.close(); } }
3. 양방향 연관관계와 연관관계의 주인
객체와 테이블 둘은 패러다임 차이가 있다.
- 객체는 참조를 사용하고 테이블은 외래키가지고 조인을하는데 이 둘간의 차이를 이해해야한다.
테이블 연관관계는 외래키 하나로 연관관계가 다있다.
MEMBER 에선 TEAM_ID로 조인 TEAM에선 TEAM_ID로 조인
하지만 단반향 객체 연관관계일경우는 Member에선 Team을 갈 수 있지만 Team에선 Member로 갈 수가 없다.
그래서 Team에다가 members를 넣어줘야 양방향으로 가능하다.
나는 team에 의해서 관리가됨. package hello.jpa; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.Persistence; import java.util.List; public class JpaMain { public static void main(String[] args) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello"); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); try{ Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setUsername("member1"); member.setTeam(team); //이러면 JPA가 알아서 TEAM에서 PK 값을 꺼내서 INSERT할때 FORGINE KEY 값을 사용한다. em.persist(member); em.flush(); em.clear(); //이렇게 하게 되면 캐시값을 초기화하고 다시 조회해서 가져온다. //이렇게 해야 깔끔하게 DB에서 값을 가져온다. //양방향 연관관계 Member findMember = em.find(Member.class, member.getId()); List<Member> members = findMember.getTeam().getMembers(); for (Member m : members) { System.out.println("m = "+ m.getUsername()); } tx.commit(); } catch (Exception e){ tx.rollback(); }finally { em.close(); } emf.close(); } }
근데 양방향이 좋냐? 그것도 아니다 객체는 왠만하면 단방향이 좋다. 신경쓸게 점점 많아진다.
mappedBy = ""의 정체 (근본적인 이유를 알고 써야한다.) 중요!!
• mappedBy = JPA의 멘탈붕괴 난이도
• mappedBy는 처음에는 이해하기 어렵다.
• 객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다
객체와 테이블이 관계를 맺는 차이
• 객체 연관관계 = 2개
• 회원 -> 팀 연관관계 1개(단방향)
회원 -> 팀 • 팀 -> 회원 연관관계 1개(단방향)
팀 -> 회원 객체는 사실 단방향 연산관계가 2개 있는 것이고 이게 양방향처럼 보이는 것이다.
• 테이블 연관관계 = 1개
• 회원 <-> 팀의 연관관계 1개(양방향)
회원 <-> 테이블 연관관계는 FORIGNE KEY값 하나로 양쪽을 다 알 수 있다.
객체의 양방향 관계
• 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단 뱡향 관계 2개다.
• 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어 야 한다.
• A -> B (a.getB())
class A { B b; }
• B -> A (b.getA()) c
class B { A a; }
테이블의 양방향 연관관계
• 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리
• MEMBER.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
둘 중 하나로 외래 키를 관리해야 한다.
객체를 두향으로 만들어놧는데 Memer에서 Team으로가는 team참조값 Team에서 Member로가는 members 참조값이 있다.
그러면 나는 도대체 둘중 뭐로 매핑을 해야할까?
member의 team값을 바꿧을 때 TEAM_ID 가 UPDATE 되야할까 ??
Team members를 바꿧을 때 TEAM_ID가 UPDATE 해야할까 ??
도대체 어디를 바꿔야하는거야? 둘다 맞는데?
사실상 DB에서는 참조를 어떻게하든 TEAM_ID만 바뀌면된다.
Member에 team에 값을 넣고 Team의 members에는 값을 안넣고,
Team에 members에 값을 넣고 Member에 team에 는 값을 안넣고,
아니면 둘다 값을 넣었을 경우 TEAM_ID는 어떻게 UPDATE 할까?
이런 문제를 해결하기위해!! 명확한 룰이 있다.
둘중하나로만 외래키를 관리해야 한다.
이게 바로 연관관계의 주인이라는 개념이다.!
연관관계의 주인(Owner)
양방향 매핑 규칙
• 객체의 두 관계중 하나를 연관관계의 주인으로 지정(Team or Member)
• 연관관계의 주인만이 외래 키를 관리(등록, 수정)
• 주인이 아닌쪽은 읽기만 가능(주인이 아닌쪽은 RO)
• 주인은 mappedBy 속성 사용X
• 주인이 아니면 mappedBy 속성으로 주인 지정
그럼 누구를 주인으로?
• 외래 키가 있는 있는 곳을 주인으로 정해라
why? -> 어떤 관계에서는 Team을 주인으로 걸 수 있다. 근데 그렇게 하면 뭐가 문제냐면?
->Team에 members에 값을 바꿧는데 다른 table에 update 쿼리가 나감
Team을 업데이트 할려했는데 update쿼리가 MEMBER가 나가는.. 이상한 경우가 생김
-> 성능에 문제가 생길수도 있다. 이유는 나중에!
-> member는 insert시 forgineKey를 바로 챙길 수 가 있어 insert 한방 쿼리가 나가는데 ,
Team은 insert나가고 Member에 update가 나갈 수 도 있다.
왜래키가 있는 곳을 주인으로 정함으로써 얻는 이점!!
-> DB입장에서 보면 외래키가 있는 곳은 무조건 N이다. 외래키가 없는 곳은 무조건 1이다.
ENTITY와 TABLE이 MAPPING되는 곳에서 연관관계가 관리된다.
즉 Member에 어떤 데이터를 바꾸면 Member가 update되는 것
• 여기서는 Member.team이 연관관계의 주인
Member의 team이 주인 Team members는 변경 불가! 일기만 가능!(조회만 가능) 양방향 매핑시 가장 많이 하는 실수 (연관관계의 주인에 값을 입력하지 않음)
Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setName("member1"); //역방향(주인이 아닌 방향)만 연관관계 설정 team.getMembers().add(member); em.persist(member);
member table 연관관계의 주인이아닌 Team에다가 값을 변경할경우 수정이되지않아 TEAM_ID가 null로 들어간다.
양방향 매핑시 연관관계의 주인에 값을 입력해야 한다.
(순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야 한다.)
Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setName("member1"); team.getMembers().add(member); //연관관계의 주인에 값 설정 member.setTeam(team); //** 이부분이 중요!!!!!!!!!!!!!!!!!!!!!!!! em.persist(member);
member table 연관관계의 주인인 Member에 team을 변경하니 변경한 값이 들어간다.
em.find(Team)이 위의 select , 아래 select는 members 부터 iter까지 JPA는 MEMBERS를 사용하는 시점에 쿼리를 한 번 날리고 forgineKey에 설정된 키값을 가지고 연관된 member를조회함.
값을 컬렉션 team.getMembers().add() 를 하지 않아도 값은 조회가 된다.
하지만 team.getMembers().add()를 하지않으면 객체 지향적이지 않게된다.
이렇게하지않으면 두군데서 문제가 발생한다.
완전히 flush, clear를 하면 문제가 발생하지 않는다.
flush clear하지않으면 이미 1차 캐시에 team.getId()가 있다.
flush, clear 하지 않으면 발생하는 문제
1.안할경우 em.find(Team.class)할때 위에서 em.persist(member) 할때 들어간 1차캐시값을 그대로 들고오게된다.
그렇기때문에 findTeam.getMembers()을 하게되면 DB에는 값이 없기때문에 아무값도 조회해오지 못한다.
team의 collection에는 아무것도 없고 메모리에도 아무것도 없다.
2. 객체 지향적으로 봣을땐 team에도 값을 세팅해주고 Member에도 값을 세팅해주는게 맞다.
3. test case를 작성할때
양방향 연관관계 주의 - 실습
• 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
• 연관관계 편의 메소드를 생성하자
• 양방향 매핑시에 무한 루프를 조심하자
• 예: toString(), lombok, JSON 생성 라이브러리
연관관계 편의 메소드
(어디기준으로 setting할지는 팀기준 멤버인지 멤버기준 팀인지는 마음대로 ~~~)
양쪽에 값을 설정해야한느데 헷깔리는 경우 연관관계 편의 메소드를 만들자! 연관관계 편의 메소드에 team.getMembers().add를 만들게되면 실행 코드를 두번 만들 필요는 없다. 연관관계 편의 메소드를 사용함으로써 Member에 Team을 세팅하는 시점에 Team에도 member를 세팅할 수 가 있다. 메소드를 원자적으로 쓸 수 있음 하나만 설저앻도 양쪽으로 다 쓸 수 있다.
김영한 선생님꼐서는 연관관계 편의 메서드나 JPA상태 변경하는 것은 set을 잘 안쓰신다.
why? -> set이 java의 getter setter 관례때문에 단순하게 로직이없이 set get할일은 없기때문에 이름을 바꾸어서 쓴다.
setTeam은 남겨두고 changeTeam으로 변경 양방향 매핑시 무한 루프를 조심하자!
• toString : 이렇게하면 team에서 team.toString을 또 호출한다. 근데 팀에가면 또 members-> member의 toString을 호출하게된다.
• lombok : lombok을하면 자동으로 toString이 생기기 때문에 제거하고 사용해야한다.
• JSON 생성라이브러리
: 앤티티를 직접 컨트롤러에서 리스펀스로 직접 보내버릴떄 양방향으로 걸려있을때 ENTITY를 JSON을 바꿀때 MEMBER 면 TEAM이있네 TEAM에선 MEMBER가 있네? 하고 파고들어감
: 컨트롤러에는 절때 entity를 반환하지마라
why? -> 컨트롤러에서 entity자체를 api 스팩에 반환해버리면
1) 무한루프가 생긴다.
2)엔티티는 충분히 변경(fieild 추가)될 수 있는데 변경하는순간 api 스팩이 바껴버림
그래서 entity는 DTO(단순하게 값만있는것)로 변환해서 반환하는것을 추천
양방향 매핑 정리
• 단방향 매핑만으로도 이미 연관관계 매핑은 완료
처음에는 무조건 단방향 메핑으로 설계를 끝내고 양방향매핑은 반대 방향으로 조회조건이 추가되는건데 JPA의 설계라 는것은 단방향 매핑만으로 이미 완료가 된 것이다. 난중에 개발하다가 필요한 몇줄 추가해서 사용하면된다.
설계의 입장으로보면 TABLE이나 객체를 MAPPING한다는 관점으로 보면 단방향매핑만으로도 설계는 끝난 것이다.
양뱡향 매핑은 생각보다 실무에서 쓸일이 많다. 그런데 중요한것은 단방향매핑만 잘해두면 양뱡향매핑은 필요한것 몇줄만 추가해주면 된다.
1. 단방향 매핑으로 다끝낸다 (설계끝 ) 2. 실제 개발단계에서 양방향 매핑을 고민하자!!
• 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추 가된 것 뿐
• JPQL에서 역방향으로 탐색할 일이 많음
• 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 됨 (테이블에 영향을 주지 않음)
연관관계의 주인을 정하는 기준
• 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안됨
• 연관관계의 주인은 외래 키의 위치를 기준으로 정해야함
'JPA' 카테고리의 다른 글
#JPA-12 상속관계 매핑 (0) 2021.02.19 #JPA-11 다양한 연관관계 매핑 (0) 2021.02.18 #JPA-9 기본 키 매핑 (0) 2021.02.16 #JPA-8 데이터베이스 스키마 자동 생성 & 필드와 컬럼 매핑 (0) 2021.02.15 #JPA-7 엔티티 매핑 (0) 2021.02.15