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); //찾은 외래키를 사용해서 다시 조회
tx.commit();
}
위의 코드처럼 객체를 테이블에 맞추어서 모델링했기 때문에 객체를 다루는것이 아니라 관계형데이터베이스와 같은 방식으로 객체를 다루고 있을 뿐 아니라 절차가 복잡하게 느껴진다.
- 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다
- 객체는 참조를 사용해서 연관된 객체를 찾는다.
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "username")
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
Member클래스 입장에서 Team과 N : 1 관계이기 때문에 @ManyToOne 어노테이션을 사용하여 객체의 참조관계와 테이블의 외래 키를 매핑한다.
try{
//팀 등록
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//맴버 등록
Member member = new Member();
member.setUsername("member1");
member.setTeam(team); // 팀의 아이디가 아닌 팀 객체를 그대로 세팅
em.persist(member);
//조회
Member findMember = em.find(Member.class, member.getId());
findMember.getTeam(); // 바로 findMember와 연관된 팀객체를 조회할 수 있다.
tx.commit();
}
...
객체지향 스럽게 바로 연관된 레퍼런스들을 가지고올 수 있다.
관계형 데이터베이스에서는 외래 키 하나로 양쪽 테이블을 조인해서 Member와 Team의 정보를 조회할 수 있었지만 객체를 사용해서는 불가능했다.
양방향 조회가 가능하도록 관계가 되는 두 객체(Entity)사이에 모두 연관관계를 설정해 줌으로써 양방향 조회,수정이 가능해진다.
💡 연관관계 주인? mappedBy?
양방향 관계에서 예를들어 수정을 해야한다면 Member클래스의 team을 수정 해야할까요? Team의 members 리스트를 수정해야 할까요? 라는 의문이 생길 수 있다.
즉 외래 키를 어디서 관리할지의 문제가 생기게 되는데 이러한 문제를 해결하기위해 외래키를 관리하지 않는 객체에 mappedBy을 사용해서 다른 객체 즉 연관관계의 주인으로부터 매핑 되어젔다 라고 설정하여 주인 객체만이 등록, 수정이 가능하고 매핑되어진 객체는 읽기만 가능하다.
💡 누구를 연관관계의 주인으로?
외래 키가 있는 곳을 주인으로 지정한다.
DB관점에서 보면 FK가 있는곳이 N이고 PK쪽이 1이다 (N : 1)
FK를 관리하는 테이블(Member)과 대응되는 객체(Member)를 연관관계의 주인으로 설정해야 update했을때 직관적으로 알기 쉽다.
💡 연관관계의 주인쪽에만 값을 세팅해주면 될까?
try {
//팀 등록
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//맴버 등록
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
Member findMember = em.find(Member.class, member.getId());
List<Member> members = findMember.getTeam().getMembers();
for ( Member mbr : members){
System.out.println("mbr = " + mbr.getUsername());
}
tx.commit();
}
위 코드에서는 정상적으로 member리스트를 조회할 수 있다 그 이유는 flush를 통해 sql문을 commit이전에 강제로 요청한 후 clear를 사용해서 1차캐시를 비워줌으로써 다음의 코드인 em.find를 통해 조회될 때 1차캐시의 데이터가 없기때문에 DB에 select문을 요청하게되고, 연관관계 매핑에 의해서 연관된 테이블을 join하여 member리스트도 select하여 1차캐시에 저장하게 된다. 이때문에 member리스트가 조회될 수 있었다.
하지만 flush와 clear를 하지 않는다면 연관관계가 설정되기 이전의 팀과 맴버 객체가 1차 캐시에 그대로 남아있기 때문에 member리스트는 아무런 값도 가지고 있지 않게된다.
이러한 문제를 해결하기위해 양방향 연관관계를 사용할 때에는 양쪽에 다 값을 세팅해주는것이 맞다.
try {
//팀 등록
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//맴버 등록
Member member = new Member();
member.setUsername("member1");
member.setTeam(team); // 연관관계의 주인쪽에 값 세팅 1
em.persist(member);
team.getMembers().add(member); // 연관관계의 주인이 아닌쪽에 값 세팅 2
//em.flush();
//em.clear();
Member findMember = em.find(Member.class, member.getId());
List<Member> members = findMember.getTeam().getMembers();
for ( Member mbr : members){
System.out.println("mbr = " + mbr.getUsername());
}
tx.commit();
}
순수 객체 상태를 고려해서 항상 양쪽에 값을 설정 해야한다.
값을 2번 설정하기때문에 실수에 여지가 있다 이러한 실수를 줄이고자 편의 메소드를 생성할 수 있다.
// Memeber
public void changeTeam(Team team){
this.team = team; //team을 설정해주는 시점에
team.getMembers.add(this); //바로 this(Member)를 member리스트에 추가
}
📚 참고 및 자료 출처 : 자바 ORM 표준 JPA 프로그래밍 - 기본편 (김영한)