Springboot 2.4.1, Spring-data-jpa, lombok을 사용하여 코드를 작성하였습니다.
연관관계는 모두 아시는것처럼 테이블과 테이블 또는 객체와 객체가 서로 참조하는 관게를 의미 합니다.
연관관계에는 3개의 키워드를 알고 가면 이해하기 쉬울것 같습니다.
방향에는 단방향,양방향이 있습니다. 말 그대로 A -> B 또는 B -> A 처럼 한쪽방향으로만 참조하는것을 단방향이라고 하며 , A -> B, B -> A 양쪽에서 서로 참조하는 것을 양방향 관계라고 한다.
N:1, 1:N, N:N, 1:1 같은 다중성들이 있습니다.
한두줄로 설명하기 어려워... 하나하나 아래에 정리해보도록 하겠습니다.
객체나 테이블을 양방향 연관관계로 만들기 위해서는 연관관계의 주인을 정해야 합니다.
연관관계의 주인은 외래키를 관리(등록,수정,삭제)등의 기능을 수행할 수있으며 주인이 아닌쪽은 읽기만 가능합니다.
단방향 연관관계는 위에 적은것처럼 A->B, B->A 처럼 한쪽방향으로만 참조하는것을 단방향 연관관계라고 부릅니다.
해당 예제는 가장 많이 사용하는 학생과 팀의 연관관계를 통해 설명해보도록 하겠습니다.
즉 회원가 팀의 관계는 다:1 관계로 보시면 됩니다
단방향 관계에서는 한쪽만이 반대쪽에 대한 정보를 알 수 있습니다. 즉, 회원쪽에서만 팀에대해서 알 수 있지 팀에서는 회원에 대한 정보를 알 수 없습니다.
JPA를 처음 사용하기 전 테이블 연관관계로 생각하였을때는 Team 의 주키인 외래키를 Member쪽에서 가지고 있으니 Team, Member의 Join을 통해 조회 할 수 있는 양방향이라 고 생각했습니다. 하지만 객체 연관관계에서는 양방향을 만들기 위해서는 반대쪽에더 똑같이 참조하는 필드를 만들어 줘야 합니다. 결과적으로는 양방향 관계처럼 보이지만 서로다른 단방향 관계를 2개 만들었다고 보시면됩니다.
Member.class 와 Team.class 2개를 만들도록 하겠습니다.
// Member.class
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idx;
private String name;
@ManyToOne
private Team team;
public Member(String name, Team team) {
this.name = name;
this.team = team;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("Member{");
sb.append("idx=").append(idx);
sb.append(", name='").append(name).append('\'');
sb.append('}');
return sb.toString();
}
}
// Team.class
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public Team(String name) {
this.name = name;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("Team{");
sb.append("id=").append(id);
sb.append(", name='").append(name).append('\'');
sb.append('}');
return sb.toString();
}
}
Member.class 부분에 보시면 @ManyToOne 을 달고 있는 Team 타입의 team이라는 변수를 확인할 수 있습니다.
이렇게 연관관계를 매핑해주기 위해서는 다음과 같은 어노테이션들을 사용해주면 됩니다. 일단 제대로 동작하는지 보고 해당 어노테이션과 또 다른 어노테이션들에 대해 알아보겠습니다.
// JpaRepository를 상속받은 Repository interface들은 생략하겠습니다.
Team team = new Team("토트넘");
teamRepository.save(team);
// Member저장 시점에서 team 객체를 저장할 때에는 연관된 모든 엔티티는 영속상태에
// 놓여 있어야합니다.
Member member = new Member("손흥민", team);
memberRepository.save(member);
// 저장된 Entity를 가져옴
Member findMember = memberRepository.findById(1L).get();
// 객체의 그래프 탐색을 통해 출력 가능
System.out.println(findMember.getTeam());
실제로 위 코드를 동작시켜 보면 분명 소스코드상에서는 memberRepository를 통해 Member만 조회한것 같습니다. 하지만 실제로 나간 쿼리는 다음과 같습니다.
select
member0_.idx as idx1_0_0_,
member0_.name as name2_0_0_,
member0_.team_id as team_id3_0_0_,
team1_.id as id1_1_1_,
team1_.name as name2_1_1_
from member member0_ left outer join
team team1_
on member0_.team_id=team1_.id
where member0_.idx=?
@ManyToOne을 통해 객체간의 연관관계를 맺고 있기 때문에 Member만 조회해도 객체 그래프를 통해 Team의 값을 같이 불러오게 됩니다.
갑자기 JoinColumn이 나와서 놀랄수도 있지만 사실 @ManyToOne밑에 적어도 되고 생략해도 됩니다. 그렇다면 @JoinColumn이 하는 역할은 무엇을까요.
@JoinColumn은 외래 키를 매핑할때 사용합니다. 해당 어노테이션의 속성은 다음과 같습니다.
기본적으로 @Column테이션이 가지고 있는 unique, nullable, insertable, updatable, columnDefinition, table 속성을 가지고 있으며 추가로 name, referencedColumnName, foreignKey와 같은 속성을 가지고 있습니다.
name
- 매핑할 외래키의 이름을 지정합니다. 기본 값은 필드명_참조하는테이블의 기본키 컬럼명
으로 설정 됩니다.
referencedColumnName
- 외래 키가 참조하는 대상 테이블의 컬럼명을 의미하며 기본 값은 참조하는 테이블의 기본 키 컬럼명으로 설정 됩니다.
referencedColumnName을 언제사용하는지에 대해 자세히 설명되어있는 블로그의 URL을 남깁니다.
foreignKey(DDL)
- 외래키 제약조건을 직접 지정 할수 있다. 해당 속성은 테이블을 생성할 때만 사용합니다.
제 소스같은 경우는 @JoinColumn을 생략하였으니 name의 기본값은 default 대로 설정되었을 것입니다.
필드명을 의미하는 team과 _(언더바) 그리고 참조하는 테이블의 컬럼명을 통하도록 되있습니다. 따러서 추측해 보자면 team_id 와 같이 매핑되어있을 것입니다.
ManyToOne 어노테이션을 말 그대로 다대일 관계에서 사용하는 어노테이션읍니다.
optional
- false 설정하면 연관된 엔티티가 항상 있어야 합니다. default 값은 true 입니다.
fetch
- 패치 전략을 설정 할 수 있습니다. FetchType.EAGER와 FetchType.LAZY가 있습니다. ManyToOne은 작성하지 않아도 기본적으로 EAGER를 사용합니다.
cascade
- 영속성 전이 기능을 사용할것인지에 대한 옵션입니다.
targetEntity
- 해당 속성은 엔티티의 타입 정보를 설정하지만 많이 사용하지 않아 따로 적지 않겠습니다 :)
지금까지 추가 및 조회만 했지만, 삭제하는 방법에대해서는 코드로 다루지 않았습니다. 글로 잠시나마 다뤄보겠습니다.
// 연관관계를 삭제하기 위해서는 null을 통해 연관관계를 제거해 주면 됩니다.
// 기존 Member.class에 메서드 추가
// 해당 메서드 실행시 소스상에서는 null이라고 했지만
// 실제로는 쓰기 지연 SQL 저장소에서 UPDATE구문을 날려 연관관계를 삭제하게 됩니다.
...
public void removeTeam() {
this.team = null;
}
그렇다면 연관된 엔티티를 삭제하기 위해서는 어떻게 해야할까요 즉, Member들에게 Team이 연관되어 있을경우에는 어떻게 Team 엔티티를 삭제해야 할까요.
해당 방법은 간단합니다. 먼저 Team에 소속되어있는 Member 엔티티들에게서 Team과의 연관관계를 끊어주는 방법이 있습니다.
member1.removeTeam();
member2.removeTeam();
teamRepository.remove(team);
위와 같이 실행하면 됩니다. 글을 쓰다보니 모든 예제코드를 포함하지 못하였지만 글 남겨주시면 더자세히 남겨드리겠습니다.
감사합니다!