• 객체의 참조와 테이블의 외래 키를 매핑
그럼 위와 같은 요구사항에 맞춰 순서대로 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());
// 여기서 문제 발생 => 연관관계가 없음
Team findTeam = em.find(Team.class, team.getId());
// 이때 sql을 보냄.
tx.commit();
위 코드는 foreign 키를 직접 다루는 것이 문제이다.
방향(Direction): 단방향, 양방향
다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)
연관관계의 주인(Owner): 객체 양방향 연관관계는 관리 주인이 필요
@ManyToOne
@JoinColumn
방법 1 과의 차이점은 Member 객체에서 참조값으로 teamId가 아닌, team 그 자체를 가져왔다.
@Data
@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;
}
이때 어노테이션들은 db에 적용되는 값들을 표시해주기 때문에 연관관계도 @ManyToOne 이라고 다대일 관계라고 명시해줘야한다.
그리고 @JoinColumn을 이용하면 나중에 비즈니스 로직을 짤 때 알아서 해당 PK 값을 가져와서 넣어준다.
이렇게 하면 추후 비즈니스로직을 동작시킬 때도 상대적으로 편리하게 Member 가 속한 Team을 확인할 수 있고 DB에는 JPA가 알아서 PK 값만 가져와서 넣어준다.
객체의 변화는 Team class에 " List members " 가 추가되었다는 것을 알 수 있다.
그러나 매우 중요한 것은 테이블 연관관계는 변한것이 하나도 없다는 것이다.
이는 사실 db에서는 연관관계에 " 방향 " 이라는 개념 자체가 없다. 어차피 그냥 fk를 등록해두면 양쪽으로 조회가 가능하기 때문이다.
@Data
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<>();
}
이렇게 하면 단대 방향으로도 객체 그래프 탐색이 가능해준다.
//조회
Team findTeam = em.find(Team.class, team.getId());
//역방향 조회
int memberSize = findTeam.getMembers().size();
그러면 여기서 중요한 것이 있다.
위 " 양뱡향 연관관계 " 의 사진을 잘 보면 " 객체 연관관계 " 는 사실 "Memeber->Team"으로 또 "Team->Member" 로 이렇게 2개의 단방향 연관관계를 묶어서 DB의 양방향 연관관계를 "흉내"낸 것이다.
여기서 문제는 DB에 있는 Team_ID(FK) 를
1. Member 객체의 Team 속성값이 바뀔 때 바꿔준다.
2. Team 객체의 List Members 속성값이 바뀔 떄 바꿔준다.
이렇게 둘 중 어는 객체가 바뀔 때 쿼리문을 날려줘야할지 문제가 생겨버린다. 즉, 둘 중 하나로 외래 키를 관리해야한다.
• 객체의 두 관계중 하나를 연관관계의 주인으로 지정
• 연관관계의 주인만이 외래 키를 관리(등록, 수정)
• 주인이 아닌쪽은 읽기만 가능
• 주인은 mappedBy 속성 사용X => mappedBy는 read 만 됨.
• 주인이 아니면 mappedBy 속성으로 주인 지정
그럼 위 사진의 정답은 나왔다.
DB를 기준으로 MEMBER 테이블에 fk가 존재하니까 Member 객체의 team 속성이 바로 주인이 되는 것이다.
즉, 정답은 1번 " Member 객체의 Team 속성값이 바뀔 때 바꿔준다. " 가 정답이 되는 것이다.
보편적으로 외래키가 있는 곳이 N : 1 외래기가 없는 곳 이렇게 돌아간다.
연관관계 주인: N 쪽 즉, 외래키가 있는 곳
연관관계의 주인에 값을 입력하지 않음.
이때 Mapped By 는 어차피 DB에 영향이 가지 않기 때문에 나중에 값을 넣어도 되지만 (그런데 넣어주는게 좋음)
연관관계의 주인 즉, @ManyToOne 어노테이션을 사용하는 속성값은 반드시 값을 넣어줘야 Member 테이블에서 FK 가 비어있는 불상사를 피할 수 있다. ( 물론 애초에 에러가 뜨겠지만 )
그리고 Mapped By도 신경써서 값을 넣어주는게 좋다. 왜냐하면 비즈니스 로직을 작성할 때 em.find 메서드로 값을 요청해도 아무것도 안 나와버리기 때문이다.
따라서 양방향 연관관계 세팅할 땐 양쪽에 값을 다 세팅해주는 것이 좋다.
이때 꿀팁은 아래 코드와 같이 연관관계 주인의 값을 세팅할 때 종속되는 값을 함께 세팅해버리는 것이다.
@Data
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
...
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public void setTeam(Team team){
this.team = team;
team.getMembers().add(this);
}
}
이때 또 주의할 점은 그냥 setTeam 하지말고 접두어를 다른 무언가(ex|change) 이런식으로 바꾸면 일반 setter 메서드와 이름도 다르고 다른 개발자로 하여금 여기는 또 다른 메서드구나 하고 인식을 시켜줄 수 있다.
물론 이때 연관관계의 주인말고 종속되는 객체에 대하여 연관관계 메서드를 생성해도 된다.
다만, 연관관계 세팅 메서드는 양쪽에 있으면 안 되고, 어느 객체를 기준으로 잡을지는 팀 내에서 정하면 된다.
정리
순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
연관관계 편의 메소드를 생성하자
양방향 매핑시에 무한 루프를 조심하자 (예: toString(), lombok, JSON 생성 라이브러리) => toString()이 양쪽 객체를 자꾸 호출하여 무한 루프에 빠진다. 답은 아래 1, 2번이다.
양방향 매핑 정리
단방향 매핑만으로도 이미 연관관계 매핑은 완료 => 처음엔 단반향 매핑으로 설계를 끝낸다. => 복잡도만 늘어난다.
다음 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다.
JPQL에서 역방향으로 탐색할 일이 많음
단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 됨 (테이블에 영향을 주지 않음)
테이블 구조가 위 사진과 같이 생겼다면 위 사진을 바탕으로 아래와 같은 객체 구조를 짤 수 있다.
하지만 여기서 Member에 orders 가 ListArray 형태로 들어가는 것은 좋지 못 한 설계라고 볼 수 있다.
코드의 복잡도가 굉장히 많이 올라가기 때문이다. 그래서 앞서 단방향 연관관계도 연관관계라고 하였다.
이를 판단하는 것은 비즈니스 적으로 혹은 로직상으로 반드시 양방향 연관관계가 필요할 때 생성해주면 되는 것이다.
그리고 사용법은 위와 같은 테이블 구조가 있다고 가정할 때 entity의 @ManyToOne과 @JoinColumn는 아래 와 같이 사용하면 된다.
@Entity
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;
...
}
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
...
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
@Enumerated(EnumType.STRING)
private OrderStatus status;
...
}
그리고 하다보니 비즈니스로직에서 뭔가 꼬여서 반드시 Member에서 Order를 조회하는 양방향이 생겼다 하면 아래와 같이 엔티티를 수정해주고
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
...
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
여기에 추가로 앞서 언급했듯 연관관계 편의(세팅) 메서드를 생성하여 양쪽에 값을 동시에 넣어줄 수 있도록 해준다.