TEAM_ID
외래 키로 팀 테이블과 연관관계를 맺는다.// 회원과 팀 조인
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
// 팀과 회원 조인
SELECT *
FROM TEAM T
JOIN MEMER M ON T.TEAM_ID = M.TEAM_ID
public class Member {
private String id;
private String username;
private Team team; // 팀의 참조를 보관
public void setTeam(Team team) {
this.team = team;
}
// Getter, Setter ...
}
public class Team {
private String id;
private String name;
// Getter, Setter ...
}
실제로 회원1과 회원2를 팀1에 소속시키는 코드는 아래와 같다.
public static void main(String[] args) {
// 생성자(id, 이름)
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
Team team1 = new Team("team1", "팀1");
member1.setTeam(team1);
member2.setTeam(team2);
Team findTeam = member1.getTeam();
}
이처럼 객체는 참조를 사용해 연관관계를 탐색할 수 있는데 이를 객체 그래프 탐색이라 한다.
// 단방향 연관관계
class A {
B b;
}
class B {}
// 양방향 연관관계
class B {
A a;
}
class A {
B b;
}
@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;
...
}
public class JpaMain {
public static void main(String[] args) {
// 엔티티 매니저 팩토리: 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
// 엔티티 매니저: 생성
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);
tx.commit(); //트랜잭션 커밋
} catch (Exception e) {
tx.rollback(); //트랜잭션 롤백
} finally {
em.close(); // 엔티티 매니저 종료
}
emf.close();// 엔티티매니저 팩토리 종료
}
}
@Entity
public class Member {
// ...
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@ManyToOne
@JoinColumn(name = "TEAM_ID")
// 팀 저장
Team team = new Team();
Team.setName("TeamA");
em.persist(team);
// 회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team); // 단방향 연관관계 설정, 참조 저장
em.persist(member);
@Entity
@Table(name="member")
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
// @Column(name = "TEAM_ID")
// private Long teamId;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@OneToMany(mappedBy = "team")
코드가 추가되었다.mappedBy
속성: 양방향 매핑일 때 사용하는 반대쪽 매핑의 필드 이름을 값으로 주면 된다.Member.team
이므로 team을 값으로 주었다.@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
결과적으로 반대방향으로도 객체 그래프 탐색이 가능해진다.
// 조회
Team findTeam = em.find(Team.class, team.getId);
int memberSize = findTeam.getMembers().size(); // 역방향 조회
mappedBy
속성은 어떤 속성이고 왜 필요할까?
mappedBy
속성을 사용하지 않는다.mappedBy
속성으로 주인을 지정해야 한다.Member.team
이 연관관계의 주인이다.public void testsave() {
// 팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team);
// 회원1 저장
Member member1 = new Member("member1", "회원1");
membre1.setTeam(team1); // 연관관계 설정 member1 -> team1
em.persist(member1);
// 회원2 저장
Member member = new Member("member2", "회원2");
member2.setTeam(team1); // 연관관계 설정 member2 -> team1
em.persist(memger2);
}
SELECT * FROM MEMBER;
MEMBER_ID | USERNAME | TEAM_ID |
---|---|---|
member1 | 회원1 | team1 |
member2 | 회원2 | team1 |
Member1.setTeam(team1); // 연관관계 설정 (연관관계의 주인)
Member2.setTeam(tesm1); // 연관관계 설정 (연관관계의 주인)
team1.getMember().add(member1); // 무시(연관관계의 주인이 아님)
team1.getMember().add(member2); // 무시(연관관계의 주인이 아님)
양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다. 데이터베이스에 외래키 값이 정상적으로 저장되지 않으면 이것부터 의심해보자.
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 Tema("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
에만 값을 저장했다.null
이 입력되었다.public void testSaveNonOwner() {
// 팀 저장
Team team = new Team()
team.setName("TeamA");
em.persist(team);
// 회원 저장
Member member = new Member();
member.setUsername("member1");
// 연관관계의 주인에 값 설정.
member.setTeam(team); //**
em.persist(member);
team.getMembers().add(member);
em.persist(team1);
}
// member(주인)에 team 매핑
member.setTeam(team); // **
// team에 member 매핑
team.getMembers().add(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(); // flush와 clear로 디비에 데이터가 입력되었다.
Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();
System.out.println("================");
for (Member m : members) { // 디비에서 값이 조회되어 출력된다.
System.out.println("m = " + m.getName());
}
System.out.println("================");
em.flush
와 em.clear
로 DB에 데이터가 추가되었고, DB의 데이터를 조회하고 있다. // 저장
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);
// DB에 데이터를 입력하지 않는다.
// em.flush();
// em.clear();
Team findTeam = em.find(Team.class, team.getId()); // 1차캐시에서 데이터 조회
List<Member> members = findTeam.getMembers();
System.out.println("================");
for (Member m : members) { // 1차 캐시에서 값을 찾지만, 값이 없다.
System.out.println("m = " + m.getName());
}
System.out.println("================");
em.flush
와 em.clear
를 주석 처리하고, Team(주인이 아닌쪽)에 값을 주입하지 않았다.📌 결론
객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주자!
member.setTeam(team); // ** 주인 연관관계 값 입력
team.getMembers().add(member);
결과적으로, 양쪽 객체 모두에게 위와 같이 값을 넣어줘야한다. 그러나 실제로 개발을 하다보면 깜빡하는 일이 더러 있을 수 있다. 이 같은 경우에 연관관계 편의메서드를 사용할 수 있다.
@Entity
public class Member {
// ...
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
member.setTeam(team)
호출 만으로 양쪽 모두 값을 주입해줄 수 있다.changeTeam
: 보는 사람 입장에서 '아, 그냥 뭔가 중요한걸 하는구나!'라고 인지할 수 있다.위와 같은 setTeam()
메서드에는 버그가 있다.
member1.setTeam(teamA); // 1
member1.setTeam(teamB); // 2
Member findMember = teamA.getmember(); // member1이 여전히 조회된다.
member1.setTeam(teamA)
member1.setTeam(teamB)
참고
- teamA -> member1의 관계가 제거되지 않아도 데이터베이스 외래 키를 변경하는 데는 문제가 없다. 왜냐하면 teamA -> member1 관계를 설정한 Team.members는 연관관계의 주인이 아니기 때문이다. 연관관계의 주인인 Member.team의 참조가 member1 -> teamB로 변경되었으므로 데이터베이스에 외래 키는 teamB를 참조하도록 정상 반영되었다.
- 이후에 새로운 영속성 컨텍스트에서 teamA를 조회해 teamA.getMembers()를 호출하면 데이터베이스 외래 키에는 관계가 끊어져 있으므로 아무것도 조회되지 않는다.
- 문제는 관계를 변경하고 영속성 컨텍스트가 아직 살아있는 상태에서 teamA의 getMembers()를 호출하면 member1이 반환된다는 점이다. 따라서 변경된 연관관계는 앞서 설명한 것처럼 관계를 제거하는 것이 안전하다.
public void setTeam(Team team) {
// 기존 팀과 관계를 제거
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
toString()
, lombok
, JSON
생성 라이브러리 코드public class Member {
@Override
public String toString() {
return "Member{" +
"id=" + id +
", username=" + name + '\'' +
", team=" + team +
'}';
}
}
public class Team {
@Override
public String toString() {
return "Team{" +
"id=" + id +
", name=" + name + '\'' +
", members=" + members +
'}';
}
}
StackOverflowError
가 발생하게 된다.toString
은 사용하지 말자.Controller
에서는 절대 Entity
를 반환하지 않는다.📌 내 정리
- 객체 연관관계:1 (단방향) & 테이블 연관관계:2 (양방향)
- 이 간극을 매우기 위해 객체에 연관관계 추가
- 왜 매꿈? 그럼 객체에서 양쪽을 다 조회가능함. 그래서 더 객체지향적 코드 작성 가능.
단방향은 항상 외래 키가 있는 곳을 기준으로 매핑하면 된다. 연관관계의 주인(Owner)이라는 이름으로 인해 비즈니스 로직상 더 중요하다고 생각되는 객체가 연관관계의 주인이라고 오해할 수 있다. 그러나 연관관계의 주인은 외래 키의 위치와 관련해서 정해야지 비즈니스 중요도로 접근하면 안된다.
@Entity
public class Member {
// ... 이전과 동일 ...
@OneToMany(mappedBy = "member");
private List<Order> orders = new ArrayList<Order>();
// Getter, Setter
...
}
@Entity
public class Order {
// 이전과 동일...
@ManyToOne
@JoinColumn(name = "MEMBER_ID");
private Member member; // 주문 회원
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems = new ArrayList<OrderItem>();
//==연관관계 메소드==//
public void setMember(Member member) {
// 기존 관계 제거
if (this.member != null) {
this.member.getOrders().remove(this);
}
this.member = member;
member.getOrders().add(this);
}
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
// Getter, Setter
...
}
setMember()
를 추가했다.Member member = new Member();
Order order = new Order();
order.setMember(member); // member -> order, order -> member
@Entity
public class OrderItem {
// 이전과 동일...
@ManyToOne
@JoinColumn(name = "Team_id)
private Item item; // 주문 상품
@ManyToOne
@JoinColumn(name = "ORDER_ID")
private Order order; // 주문
}
@Entity
public class Item {
@Id @GeneratedValue
private Long id;
..
}
// 주문한 회원을 객체 그래프로 탐색
Order order = em.find(Order.class, orderId);
Member member = order.getMember(); // 주문한 회원, 참조 사용
// 주문한 상품 하나를 객체 그래프로 탐색
Order order = em.find(Order.class, orderId);
orderItem = order.getOrderItems().get(0);
Item = orderItem.getItem();