객체 연관관계
테이블 연관관계
객체 연관관계와 테이블 연관관계의 차이
순수한 객체 단방향, 다대일(N:1) 클래스
순수한 객체 단방향, 다대일(N:1) 인스턴스
객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라 한다
데이터베이스는 외래 키를 사용해서 연관관계를 탐색할 수 있는데 이것을 조인이라 한다
@Entity
public class Member {
@Id
@Column(name ="MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
}
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private Long id;
private String name;
}
@ManyToOne: N:1 연관관계
@OneToMany: 1:N 연관관계
@JoinColumn: 조인 컬럼은 외래키를 매핑할 때 사용, 생략가능
@ManyToMany: N:N 연관관계
대부분 릴레이션 테이블을 이용하여 N:1 , 1:N 관계로 이용
@OneToOne: 1:1 연관관계
외래키를 매핑할 때 사용
name: 매핑할 외래키 이름
referencedColumnName: 외래키가 참조하는 대상 테이블의 컬럼 명
foreignKey(DDL): 외래 키 제약조건을 직접 지정 가능
테이블을 생성할 때만 새용
unique,nullable,insertable,updatable,columnDefinition,table: @Column의 속성과 같다
다대일 관계에서 사용
optional: false로 설정하면 연관된 엔티티가 항상 있어야 한다
fetch: 글로벌 패치 전략 설정 (EAGER, LAZY)
cascade: 영속성 전이 기능 설정
targetEntity: 연관된 타입 정보 설정
JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태이어야 한다
연관관계가 있는 엔티티 조회 방법은 2가지이다
객체 그래프 탐색 (객체 연관관계를 사용한 조회)
Member member = em.find(Member.class, "member");
Team team = member.getTeam(); // 객체 그래프 탐색
객체지향 쿼리 사용 (JPQL)
private static void query(EntityManager em) {
String jpql = "select m from Member m join m.team t where t.name = :teamName";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setParameter("teamName", "팀1")
.getResultList();
수정은 em.update() 같은 메소드가 없다. 단순히 불러온 엔티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어난다.
member.setTeam(null); // 연관관계 제거
엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다. 그렇지 않으면 외래 키 제약조건으로 인해 데이터베이스에서 오류가 발생
team = {member1, member2}
member1.setTeam(null);
em.remove(team); // member2 가 남아있어 에러 발생
member1.setTeam(null);
member2.setTeam(null);
em.remove(team); // 정상작동
데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할 수 있다.
@Entity
public class Member {
@Id
@Column(name ="MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
}
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
}
mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다.
객체의 양방향 연관관계는 단방향이 2개라고 생각하면 된다. 그래서 ManyToOne에 외래키를 지정해주었음에도 OneToMany에 mappedBy 속성이 필요하다 !
엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래키는 하나다. 따라서 둘 사이에 차이가 발생한다. 이러한 차이로 인해 JPA에서는 두 객체 연관관계중 하나를 정해서 테이블의 외래키를 관리해야하는데 이것을 연관관계 주인(Owner) 라고 한다.
연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래키를 관리할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
mappedBy를 사용하여 연관관계의 주인을 지정할 수 있다.
연관관계의 주인은 테이블에 외래키가 있는 곳으로 정해야 한다.
다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy 속성이 없다.
team.getMembers().add(member); // 무시 (연관관계의 주인이 아님)
주인이 아닌 곳에 입력된 값은 외래키에 영향을 주지 않는다. 코드는 데이터베이스에 저장할 때 무시된다.
member.setTeam(team); // 연관관계 설정 (연관관계 주인)
Member.team은 연관관계의 주인이다. 엔티티 매니저는 이곳에 입력된 값을 사용하여 외래 키를 관리한다.
흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다.
team.getMembers().add(member); // member의 team에 null 값이 입력됨
객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
member.setTeam(team);
List<Member> members = team.getMembers();
// result = empty
member.setTeam(team);
team.getMembers().add(member);
List<Member> members = team.getMembers();
// result = member 1명
결론 : 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주자 !!
양방향 관계에서 두 코드는 하나인 것처럼 사용하는 것이 안전하다
public class Member {
private Team team;
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
이렇게 한 번에 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드라 한다
setTeam() 메소드에는 버그가 있다
member.setTeam(team1);
member.setTeam(team2);
Member findMember = team1.getMember(); // team2로 바뀌었으나 여전히 조회가 됨
삭제되지 않은 관계
team1 → team2로 변경할 때 team1의 관계를 제거하지 않았다.
즉, 기존팀과 연관된 관계를 삭제하는 코드를 추가해주어야 한다.
public void setTeam(Team team) {
if(!ObjectUtils.isEmpty(this.team)){
this.team.getMembers().remove(this):
}
this.team = team;
team.getMembers().add(this);
}
👍 참고사항
삭제되지 않아도 데이터베이스 외래 키를 변경하는 데에는 문제가 없다.
하지만 영속성 컨텍스트가 아직 살아있는 상태에서 호출하면 문제가 발생하는 것!
⚠️ 주의사항
양방향 매핑시에는 무한루프에 빠지지않게 주의 !
이런 문제는 엔티티를 JSON으로 변환할 때 자주 발생. JSON 라이브러리들은 무한루프에 빠지지 않도록 하는 어노테이션이나 기능을 제공.
혹은 Rs안에 Entity가 아닌 Dto를 넣어 해결하도록 하자
👍 참고사항
일대다를 연관관계의 주인으로 선택하는 것은 불가능하지는 않다. 하지만 성능관리 측면에서 권장하지 않는다. 될 수 있으면 외래키가 있는 곳을 연관관계의 주인으로 선택하자.