엔티티들은 다른 엔티티들과 연관관계를 맺고 조회한다.
테이블에서는 외래 키를 사용하여 서로의 테이블을 조회하는 것이 가능하고, 객체는 다른 객체와의 참조를 통해 조회하는 것이 가능하다.
객체의 경우 서로 다른 객체 중 참조가 있는 객체 쪽에서만 조회가 가능하다.
방향 : 단방향과 양방향 매핑이 있으며, 서로 다른 두 개의 엔티티 중 한 개의 엔티티 쪽에서만 참조하는 경우를 단방향, 모두 서로의 객체를 참조하는 경우를 양방향 관계라고 한다. 테이블의 경우 외래키를 이용해 서로의 테이블 데이터를 조회할 수 있기 때문에 사실상 양방향 관계라고 볼 수 있다.
다중성 : 테이블의 관계와 동일하다. 1:N , N:1, 1:1, N:M 관계를 의미한다.
연관관계의 주인(owner) : 객체를 양방향 관계로 맺어 서로를 참조하는 경우 서로의 연관관계의 주인을 정해줘야 한다.
단방향 연관관계
객체 연관관계와 테이블 연관관계를 비교해보자.
객체 연관관계는 회원 엔티티에서 팀 엔티티를 참조하는 것으로 팀 엔티티의 필드를 조회할 수 있지만, 팀 엔티티는 회원 엔티티를 참조하고 있지 않기 때문에 회원 엔티티의 데이터를 조회할 수 없다. 즉 단방향 관계이다.
반대로 테이블 연관관계는 TEAM_ID라는 외래 키를 이용해 회원 테이블과 팀 테이블을 조인해 두 개의 테이블 데이터를 모두 조회하는 것이 가능하다. 즉 양방향 관계이다.
이를 통해, 참조를 통한 연관관계를 맺는 것은 항상 단방향 연관관계임을 알 수 있다.
객체의 양방향 관계를 맺어주기 위해서는 양쪽에서 서로를 참조하는 것으로 맺는 것이 가능하며, 이는 양방향 관계가 사실상 서로 다른 단방향 관계 2개가 합쳐진 것임을 알 수 있다.
위의 그림은 한 명의 회원이 하나의 팀에 속할 수 있고, 하나의 팀에는 여러 명의 회원이 소속되는 다대일 관계이다.
테이블 관계를 보고 객체를 매핑하면 아래와 같이 참조로 나타낼 수 있다.
@Entity
@Table(name = "MEMEBER")
public class Member{
@Id @GenerateddValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne // 다대일 관계(다중성) 연관관계 매핑
@JoinColumn(name = "TEAM_ID") // TEAM 테이블의 TEAM_ID와 매핑(외래키)
private Team team;
// 연관관계 설정
public Team setTeam(Team team) {
this.team = team;
}
}
@ManyToOne을 이용해 회원 엔티티와 팀 엔티티의 연관관계를 매핑해주었다.
@JoinColumn을 이용해 실제 DB의 Team 테이블의 어떤 외래키와 매핑되는지 설정해주었다.
Setter 메소드를 이용해 회원 엔티티를 저장 시에 참조할 팀 엔티티를 파라미터로 넣어주어 연관관계를 설정해주었다.
일반적으로 엔티티 클래스에서 Setter를 사용하는 것은 권장되지 않기 때문에, 생성자 혹은 @Builder를 이용하는 것이 바람직하다.
연관 관계에서의 CRUD
Create (저장)
다른 엔티티와 연관관계가 설정한 뒤 저장하는 경우 저장된 엔티티를 참조하는 엔티티 필드에 넣은 후 참조하는 엔티티를 저장한다.
회원 엔티티는 setter를 이용해 팀 엔티티를 참조한 상태로 영속성 컨텍스트에 persist 해준다.
public void save() {
// Team 엔티티 생성 및 저장
Team team = new Team("teamId", "teamA");
em.persist(team);
// 회원 엔티티 생성 및 연관관계 설정 및 저장
Member member = new Member("memberId", "memberA");
// 영속성 컨텍스트에 team 엔티티 참조값을 세팅한다.
member.setTeam(team);
em.persist(member);
}
Read (조회)
연관된 엔티티를 조회하는 방식은 객체 그래프 탐색, JPQL을 이용해 조회할 수 있다.
- 객체 그래프 탐색 : 참조한 엔티티의 필드에 접근해서 값을 조회한다.
Member findMember = em.find(Member.class, "memberId");
Team findTeam = findMember.getTeam(); // 회원 엔티티는 팀 엔티티를 참조하고 있기 때문에 조회가 가능하다.
- JPQL : 객체 지향 쿼리를 이용해 조회한다.
String jpql = "select m from Member m join m.team t where t.name = :teamName";
List<Member> findMembers = em.createQuery(jpql, Member.class).setParameter("teamName", "teamA").getResultList();
Update (수정)
단순 엔티티 수정과 비슷하다. 영속성 컨텍스에서 조회한 엔티티에 참조할 새로운 엔티티를 설정해주면 된다.
Team newTeam = new Team("team2", "teamB");
em.persist(newTeam);
Member findMember = em.find(Member.class, "memberId"); // 현재 TeamA 참조 중
findMember.setTeam(newTeam); // 변경 감지를 통해 커밋 시 update 쿼리 발생
Delete (삭제)
삭제 시에는 기존에 있던 연관관계를 제거한 후 삭제해야한다.
연관관계를 제거하지 않을 시 데이터베이스는 외래키가 존재한다고 판단하기 때문에 오류를 발생 시킨다.
Member findMember = em.find(Member.class, "memberId"); // 현재 TeamB 참조 중
findMember.setTeam(null); // 연관관계 끊기
em.remove(team) // Team B 삭제
양방향 연관관계
한 명의 회원은 하나의 팀에 속하고 하나의 팀에는 여러 명의 회원이 속할 수 있는 다대일 관계이다.
테이블은 기본적으로 외래 키를 이용해 양방향으로 서로의 테이블을 조인해 사용할 수 있지만, 객체는 서로의 엔티티의 참조 필드를 줌으로 써 조회가 가능하다.
즉, 엔티티는 서로의 엔티티를 참조하는 필드를 갖는 단방향 연관관계 2개가 있다고 보면 된다.
양방향 연관관계라고 해서 데이터베이스 내의 테이블 구조가 변동되는 것은 하나도 없다.
엔티티 클래스들의 코드만 변경된다. 회원 엔티티는 단방향 관계에서와 동일한 코드이다.
@Entity
@Table(name = "MEMEBER")
public class Member{
@Id @GenerateddValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne // 다대일 관계(다중성) 연관관계 매핑
@JoinColumn(name = "TEAM_ID") // TEAM 테이블의 TEAM_ID와 매핑(외래키)
private Team team;
// 연관관계 설정
public Team setTeam(Team team) {
this.team = team;
}
}
팀 엔티티는 양방향 관계에서 회원 엔티티를 참조하기 위해서 @OneToMany를 이용해 일대다 관계를 설정해준다.
하나의 팀에는 여러 명의 회원이 소속되기 때문에 일대다 관계이며, 컬렉션을 이용한다.
엔티티에서 컬렉션을 사용할 때 초기화를 해주는 것이 관례이다. 초기화를 해줌으로 써 NPE를 피할 수 있다.
@Entity
@Table(name = "TEAM")
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
@Column
private String name;
// 초기화가 관례
// 연관관계의 주인이 아니기 때문에 읽기만 가능
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
연관관계의 주인
양방향 연관관계 시 가장 중요한 부분은 두 연관관계 중 연관관계 주인을 정해주는 것이다.
데이터베이스는 외래 키 한 개를 이용해 양방향 조회가 가능하지만, 엔티티의 양방향 관계는 두 개의 참조를 통해 이뤄진다.
즉, 데이터베이스 관점에서는 외래 키가 두 개가 있어야 하는 것이다.
이 부분에서, 엔티티와 실제 데이터베이스 간의 패러다임의 차이를 보인다.
따라서, 이 두 연관관계 중 외래 키를 실제로 관리해 데이터베이스의 등록, 수정, 삭제를 수행할 수 있는 연관관계의 주인을 설정하는 것이 필요하다.
연관관계의 주인은 실제 외래키를 관리하고 데이터베이스의 등록, 수정, 삭제를 수행하는 반면, 주인이 아닌 엔티티는 조회만 가능하다.
연관관계의 주인은 외래키를 실제로 관리하기 때문에 실제 데이터베이스 내에서 외래키를 관리하는 테이블과 매핑되는 엔티티를 연관관계 주인으로 설정한다.
연관관계의 주인이 아니면 mappedBy 속성을 이용해 연관관계의 주인을 지정한다.
// Team
@OneToMany(mappedBy = "team") // mappedBy의 값은 주인 엔티티의 실제 참조 필드변수명이다.
private List<Member> members = new ArrayList<>();
// Member
@ManyToOne // 다대일 관계(다중성) 연관관계 매핑
@JoinColumn(name = "TEAM_ID") // TEAM 테이블의 TEAM_ID와 매핑(외래키)
private Team team; // mappedBy에 지정되는 필드명
// 연관관계의 주인은 외래키를 관리하는 Member가 되며, Team은 mappedBy를 이용해 연관관계의 주인을 지정한다.
데이터베이스 테이블의 다대일, 일대다 관계에서는 ‘다’ 쪽이 외래 키를 갖기 때문에 @ManyToOne가 항상 연관관계의 주인이된다. 따라서 mappedBy 속성 또한 없다.
양방향 연관관계의 주의점
양방향 연관관계에서 엔티티의 저장은 단방향 연관관계와 동일하다.
양방향 연관관계는 연관관계의 주인이 외래키를 관리하기 때문에 연관관계의 주인에게 값을 바드시 설정해주어야 한다.
연관관계의 주인이 아닌 방향은 값을 세팅하더라도 데이터베이스 저장 시에 외래 키에 영향을 주지 않으므로 무시된다.
public void save() {
// Team 엔티티 생성 및 저장
Team team = new Team("teamId", "teamA");
em.persist(team);
// 회원 엔티티 생성
Member member = new Member("memberId", "memberA");
member.setTeam(team); // 연관관계 주인이 외래키를 관리
team.getMember().add(member); // 연관관계 주인이 아니므로 외래키에 영향을 주지 않음
em.persist(member);
}
하지만, 연관관계의 주인이 아닌 방향에만 값을 세팅한다면 실제 데이터베이스의 외래 키를 관리하지 않기 때문에, 외래 키의 값이 null로 입력된다.
Member member = new Member("memberId", "memberA");
//member.setTeam(team); // 연관관계 주인이 외래키를 관리
team.getMembers().add(member); // 연관관계 주인이 아닌 경우만 값 설정 시 외래키 null문제
em.persist(member);
연관관계의 주인에만 값을 설정하더라도 외래 키가 저장되고, 연관관계 주인이 아닌 방향에는 값을 세팅하더라도 실제 데이터베이스 저장에는 영향이 없다.
하지만 이런 경우 객체의 관점에서 보았을 때, 연관관계의 주인인 쪽만 값이 세팅되어있다.
연관관계의 주인 입장에서는 값이 정확하게 세팅되어있지만, 실제로 연관관계 주인이 아닌 방향에서 연관관계 주인의 엔티티를 조회할 때는 값을 조회할 수가 없다.
Member memberA = new Member("memberId", "memberA");
Member memberB = new Member("memberId2", "memberB");
Team team = new Team("teamId", "teamA");
// 연관관계 주인이 값 설정
memberA.setTeam(team);
memberB.setTeam(team);
// 결과 : 외래키 저장 정상, 팀A에는 memberA, memberB가 소속되어있다.
// 연관관계 주인이 아닌 방향
team.getMembers().size();
// 결과 : 0 - 팀A에는 memberA, memberB가 소속되어있기 때문에 결과가 2가 나와야하는게 바람직
따라서, 양방향 연관관계에서는 연관관계의 주인 방향과 주인이 아닌 방향에 모두 값을 설 정해주 주는 것이 중요하다.
memberA.setTeam(team);
team.getMembers().add(memberA);
memberB.setTeam(team);
team.getMembers().add(memberB);
// 연관관계 주인이 아닌 방향
team.getMembers().size(); // 2
연관관계 편의 메소드
위와 같이 양방향 연관관계에서 연관관계에 있는 모든 방향에 값을 설정해 관계를 설정해주는 것이 중요하다고 했다.
하지만, 연관관계를 설정하는 코드를 각각 사용하다 보면, 누락이 되거나 하는 실수를 통해 양방향 연관관계가 깨질 수 있다.
따라서, 해당 코드들을 한 번에 관리할 수 있도록 메소드로 추출한 것을 연관관계 편의 메소드라고 한다.
연관관계 편의 메소드는 연관관계의 주인이나 주인이 아닌 엔티티 쪽 아무 곳에서나 호출을 해도 무방하다.
// 기존 연관관계 설정
memberA.setTeam(team);
team.getMembers().add(memberA);
// Member (연관관계 주인 입장)
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this); // this는 Member Class
}
// Team (연관관계 주인이 아닌 방향)
public void addMember(Member member) {
this.members.add(member);
member.setTeam(this);
}
연관관계 편의 메소드를 작성 시에 연관관계를 변경하는 경우에 기존 연관관계를 끊어주는 것이 필요하다.
// 기존
member1.changeTeam(teamA); // member1 <-> teamA
teamA.getMember(); // member1
member1.changeTeam(teamB); // member1 <-> teamB
teamA.getMember(); // member1 이미 연관관계가 끊어진 상태여야하는데 조회가됨
위와 같은 경우 연관관계의 주인 입장(member1)에서는 기존 연관관계를 끊어내고, 새로운 연관관계(teamB)를 맺었다.
이렇게까지만 하고, 커밋이 되어 새로운 영속성 컨텍스트 내에서 TeamA가 조회하는 것은 연관관계 주인(member1)이 이미 데이터베이스내에 외래키를 teamB로 업데이트 했기 때문에 문제가 되지 않는다.
하지만, 커밋이 되기 이전 기존 연관관계(TeamA)와 새로운 연관관계(TeamB)가 동일한 영속성 컨텍스트 내에서 활용되고 있는 경우 TeamA는 관계를 완전하게 끊어내지 못한 상황이다.
즉, TeamA가 회원을 조회하면 이미 끊어진 관계임에도 관계가 설정된 것처럼 보이는 문제가 발생한다.
따라서, 연관관계 편의 메소드를 작성할 때는 기존 연관관계가 있는지를 판단하여 연관관계가 있는 경우 기존 연관관계를 먼저 삭제하고, 새로운 연관관계로 설정해주는 것이 필요하다.
// Member (연관관계 주인 입장)
public void changeTeam(Team team) {
if(this.team != null) {
this.team.getMembers.remove(this);
}
this.team = team;
team.getMembers().add(this); // this는 Member Class
}
// Team (연관관계 주인이 아닌 방향)
public void addMember(Member member) {
if(!this.members.contains(member) {
this.member.team = null;
}
this.members.add(member);
member.setTeam(this);
}
📄 References
김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 : https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
'Backend > JPA' 카테고리의 다른 글
엔티티 설계의 주의사항 간단 정리 (0) | 2022.07.05 |
---|---|
다양한 연관관계 매핑 (0) | 2022.05.02 |
기본키 매핑, @Id, @GeneratedValue (0) | 2022.04.19 |
필드와 컬럼 매핑, @Column (0) | 2022.04.19 |
객체와 테이블 매핑, @Entity, @Table (0) | 2022.04.19 |