• 방향(Direction): 단방향, 양방향
• 다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이해
• 연관관계의 주인(Owner): 객체 양방향 연관관계는 '관리 주인'이 필요
사실상 객체지향스럽게 설계하는 것에 근본적으로 이해하는 것이 중요하단 것을 잊지말고 시작하자.
• 회원과 팀이 있다.
• 회원은 하나의 팀에만 소속될 수 있다.
• 회원과 팀은 다대일 관계다.
객체는 테이블의 외래키 값을 그대로 가져와서 쓴다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@Column(name = "TEAM_ID")
private Long teamId;
…
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
…
}
직접적으로 전달해준건데
@Column(name = "TEAM_ID") private Long teamId;
문제: MEMBER랑 TEAM을 레퍼런스로 참조를 주는게 아니라 DB를 기준으로 맞추어 외래키만 가져와 넣어주었다.
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("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);
연관관계 매핑을 쓰지 않았기 때문에
계속 db를 통해서 끄집어내야 되고 이러다 보니 객체지향스럽지가 않음.
정리하면
그래서 연관관계가 나왔다.
이전처럼 테이블에 맞추어 모델링하지 않고 '객체지향'스러운 모델링을 만들어 보자.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
// @Column(name = "TEAM_ID")
// private Long teamId;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
- 이전처럼 FK를 직접 때려 넣어주는 코드가 아닌, 객체를 넣고 이것과 매핑되는 관계
-> 즉, 연관관계에 대해 구체적으로 적어준다.
이 Member 클래스는 Team 클래스는 1개의 팀에 여러 명의 회원이 소속되기 때문에 '다 대 일'의 관계가 된다.
@ManyToOne
으로 관계를 적어주고.@JoinColumn(name = "상대 클래스에 매핑되는 필드명")
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team); //단방향 연관관계 설정, 참조 저장
em.persist(member);
//조회
Member findMember = em.find(Member.class, member.getId());
//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam()
코드도 전보다 더 간결해졌다.
findMember.getTeam()
으로 바로 꺼내서 쓸 수 있따.
=> 한 방에 다 땡겨왔다
Member findMEmber = em.find(MEmber.class, member.getId());
Team findTeam = findMember.getTeam();
//(문제) : Team에서 Member를 바로 가져올 수 없음
//findTeam.getMember();
-> (양방향이였따면...) 레퍼런스만 넣어두면 둘 다 와리가리 왔다갔다가 가능했을 것이다
우리가 들어가기 전 봐야할게 테이블과 객체는 둘이 다르다..
@OneToMany
: Team(1) 기준에서 Member(다)와의 관계@OneToMany(mappedBy = "연관관계의 주인의 필드")
• 회원 -> 팀 연관관계 1개(단방향)
• 팀 -> 회원 연관관계 1개(단방향)
• 회원 <-> 팀의 연관관계 1개(양방향)
양방향이라는 것은 이렇게 2가지의 관계가 합쳐져 나온 것이다.
DB 입장에서는 참조가 어찌되든 간에 FK의 업데이트만 되면 되는 것이다.
• 객체의 두 참조 관계 중 하나를 연관관계의 주인으로 지정
• 연관관계의 주인만이 외래 키를 관리(등록, 수정)
• 주인이 아닌 쪽은 읽기만 가능
=> 곧 mappedBy
가 써져있는 쪽은 읽기만 됨
• 주인은 mappedBy
속성 사용해선 X
• 주인이 아니면, mappedBy
속성으로 주인 지정
정답 : '외래키'가 있는 곳
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<Member>();
Member가 주인이 되어야 한다.
그러니 Team 클래스에 mappedBy가 설정된다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
//연관관계의 주인은 Member인데 team에서 설정?
team.getMembers().add(member);
em.persist(member);
결과는
team.getMembers().add(member);
//Member(연관관계의 주인)에 값 설정
member.setTeam(team); //추가해줌
em.persist(member);
왜?
- 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
//기존에
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
member.setTeam(team); //** 세팅에 필요
em.persist(member);
//team.getMembers().add(member);//** 세팅에 필요
em.flush();
em.clear();
.
.
//세팅에 필요한 부분을 연관 메소드로 만들어 줌
//Member 클래스 편의 메소드
public void changeTeam(Team team){
this.team = team;
team.getMembers().add(this);//this는 자기자신
}
//혹은 Team 클래스에 편의 메소드
public void addMember(Member member){
member.setTeam(this);
members.add(member);
}
- 연관메소드도 둘 중 한 곳에만 정해서 써줘야 한다. => 값을 어디에 세팅할지는 본인 맘
ex) toString(), lombok, JSON 생성등
양쪽에 toString() 등으로 무한호출을 해버림
(보통 이럴 때)Controller를 Entity로 바로 Response를 반환할 때 Entity가 양방향 연관관계인 경우에 생긴다.
(원인) 엔티티를 JSON으로 바꿔주면서 장애가 남
(해결) lombok에서 toString()은 웬만해서 쓰지마라,
JSON 라이브러리는 걱정 ㄴㄴ
-> Controller는 Entity로 반환 안해주면 됨. (무한루프, 엔티티 중간변경 위험)
차라리 Entity를 DTO(값만 있는애)로 변환해서 반환하는 것을 추천
• '단방향 매핑'만으로도 이미 연관관계 매핑은 완료
-> 객체 입장에선 양방향으로 만들면 오히려 고민이 더 많아질 뿐
• '양방향 매핑'은 그저 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
• JPQL에서 역방향으로 탐색할 일이 많음
• 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 됨(테이블에 영향을 주지 않음)
-> 사실상 자바코드 몇 줄 넣는 것은 크게 어렵지 않다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
private String city;
private String street;
private String zipcode;
//얘는 굳이 싶긴하다 꼭 안 넣어줘도 되는데
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
//getter,setter 생략
}
@Entity
@Table(name = "ORDERS")
public class Order {
@Id
@GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID") //연관관계 주
private Member member;
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems = new ArrayList<OrderItem>();
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status;
//편의메소드
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
//getter, setter 코드
}
@Entity
@Table(name="ORDER_ITEM")
public class OrderItem {
@Id
@GeneratedValue
@Column(name="ORDER_ITEM_ID")
private Long id;
@ManyToOne
@JoinColumn(name="ORDER_ID")
private Order order;
@ManyToOne
@JoinColumn(name="ITEM_ID")
private Item item;
private int orderPrice;
private int count;
//getter,setter 생략
}
@Entity
public class Item {
@Id
@GeneratedValue
@Column(name="ITEM_ID")
private Long id;
private String name;
private int price;
private int stockQuantity;
//getter,setter 생략
}