단방향 연관관계
만약 위와 같은 연관관계의 테이블이 있다고 했을 때
멤버는 하나의 팀에만 소속될 수 있지만
팀은 여러 명의 회원을 가질 수 있는 다대일(멤버:팀) 관계이다.
테이블과의 연관관계는 한 쪽에서만 외래키를 참조하면
어느 쪽에서든 조인할 수 있는 양방향 관계이다.
@Entity
@Table
@Getter @Setter
public class Member {
@Id
@Column(name = "member_id")
private int id;
@Column(name = "member_name")
private String memberName;
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
@Entity
@Table
@Getter @Setter
public class Team {
@Id
@Column(name = "team_id")
private int Id;
@Column(name = "team_name")
private String teamName;
}
하지만 객체에서는 참조하고자 하는 필드에서
팀 객체를 필드로 선언하여 연관관계를 맺는다.
테이블과는 다르게 객체에서의 연관관계의 방향성은
객체를 참조한 쪽에서만 접근할 수 있는 단방향 관계다.
참조를 통한 연관관계는 언제나 단방향이기에 양방향으로 만들려면
반대쪽에서도 객체 참조를 해줘야한다.
하지만 이건 엄밀히 따진 서로 다른 단방향 관계 두 개라고 보는게 맞다.
Member member1 = new Member(1,"고길동");
Member member2 = new Member(2,"신짱구");
Team team1 = new Team(1,"팀1");
member1.setTeam(team1); // Member 객체의 Setter를 통해 연관관계 설정
member2.setTeam(team1);
Team findTeam = member1.getTeam(); // Member 객체의 Getter를 통해 객체 참조 탐색
객체는 위와 같이 참조를 사용해 연관관계를 탐색하며
이러한 탐색을 객체 그래프 탐색이라고 한다.
다대일 관계 매핑
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
참조하고자 하는 객체의 필드에서 참조하려는 객체를 필드에 선언한다.
해당 필드에는 @ManyToOne 어노테이션과 @JoinColumn 어노테이션을 추가한다.
@ManyToOne
다대일 관계를 정의할 때 사용하는 어노테이션으로
하나의 팀(One)에 여러 멤버(Many)가 속할 수 있으니
위의 코드에서는 Many(Member) To One(Team)이라고 볼 수 있다.
아래와 같은 속성들을 사용할 수 있다.
- optional : 기본값은 true로 false로 설정하면 연관된 엔티티가 항상 있어야 함
- fetch : 글로벌 패치 전략을 설정한다. 기본값은 FetchType.Eager
- cascade : 영속성 전이 기능을 사용한다
- targetEntity : 연관된 엔티티의 타입 정보를 설정한다. 거의 사용하지 않음
@JoinColumn
외래키 를 매핑할 때 사용하는 어노테이션
아래와 같은 속성들을 사용할 수 있다.
- name : 매핑할 외래 키 이름을 지정하며 미지정시 필드명_참조하는 기본 키 컬럼명
- referencedColumnName : 외래 키가 참조하는 대상 테이블의 컬럼명
- foreignKey(DDL) : 외래 키 제약조건을 직접 지정하며 테이블을 생성할 때만 사용
- 추가로 @Column 어노테이션의 속성도 사용가능
연관관계 사용
저장
Team team1 = new Team(1, "팀1"); // 비영속
em.persist(team1); // 영속
Member member1 = new Member(1, "홍길동"); // 비영속
member1.setTeam(team1); // 비영속 연간관계 설정
em.persist(member1); // 영속
JPA에서 엔티티를 저장하려면 연관된 모든 엔티티까지 영속 상태여야 하기 때문에
팀과 멤버 엔티티를 모두 영속 상태로 만든다.
조회
연간관계가 있는 엔티티의 조회 방법은 두 가지가 있다.
Team findTeam = member1.getTeam();
첫 번째 방법은 이전에 살펴보았던 객체 그래프 탐색이 있다.
필드에 참조하고 있는 객체를 사용하여 조회한다.
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();
두 번째 방법은 객체지향 쿼리인 JPQL을 사용하는 방법이다.
객체 참조 방식으로는 데이터베이스의 조인을 사용할 수 없었지만
이 방식을 사용하면 조인을 사용하여 조회할 수 있다.
String jpql = "select m from Member m join m.team t where t.name = :teamName"
위의 코드를 살펴보면 변수 jpql에 쿼리문을 담아두는데
여기서 사용되는 m과 t는 각각 멤버와 팀 테이블의 별칭을 지정한 것이며
:teamName 같은 부분은 이후에 파라미터로 값을 받아올 부분이다.
쿼리문으로 변환하면 아래와 같다.
SELECT * FROM MEMBER AS M JOIN TEAM ON M.TEAM_ID = T.TEAM_ID
WHERE TEAM_NAME = "파라미터로 받아올 팀명";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setParameter("teamName", "팀1");
.getResultList();
해당 팀에 속한 멤버 엔티티를 가져오는 쿼리문이니
멤버 엔티티를 담기 위한 멤버 타입의 리스트를 선언한다.
해당 리스트에 엔티티 매니저의 createQuery 메서드를 사용하여
기존에 만들어둔 쿼리문을 넘겨주고 반환 타입을 지정해준다.
쿼리문의 :teamName 파라미터에 값을 넘겨주기 위해
setParameter 메서드를 사용하여
어느 파라미터에 어떤 값을 넘겨줄지 지정해준다.
getResultList 메서드를 사용하여 쿼리문의 결과를
리스트 형태로 가져온다.
수정
Team team2 = new Team(2, "팀2");
em.persist(team2);
Member member = em.find(Member.class, 1);
member.setTeam(team2);
새로운 팀을 만들고 기존에 팀1에 속해있던 member_id가 1인 멤버를 팀2로 수정한다.
영속성 컨텍스트에서 Setter를 통해 엔티티의 값을 수정하면
변경 감지 기능이 자동으로 update 쿼리문을 생성해주기 때문에
위와 같이 간단한 작업만으로 수정할 수 있다.
제거
Member member = em.find(Member.class, 1);
member.setTeam(null);
연관관계 제거도 수정과 마찬가지로 기존의 연관관계를 null로 바꿔주기만 하면 된다.
연관된 엔티티 삭제
member1.setTeam(null);
member2.setTeam(null);
em.remove(team);
연관된 엔티티를 삭제하려면 기존에 있던 연관관계들을 모두 제거해야 한다.
만약 팀1을 삭제하려면 위의 코드처럼
팀1에 속한 멤버와의 연간관계를 모두 제거한 후에
해당 팀을 삭제해야 외래 키 제약조건에 걸리지 않고
연관된 엔티티를 삭제할 수 있다.
양방향 연관관계
양방향 연관관계 매핑
게시글 초반에 언급했던 것처럼 양방향 연관관계를 맺으려면
다른 반대 쪽에도 단방향 연관관계를 추가해주면 된다.
@Entity
@Table
@Getter @Setter
public class Team {
@Id
@Column(name = "team_id")
private int Id;
@Column(name = "team_name")
private String teamName;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
}
멤버는 여러 멤버가 하나의 팀에 속하는 것이니 @ManyToOne 어노테이션을 사용했다면
팀은 하나의 팀이 여러 멤버를 가지니 @OneToMany 어노테이션을 사용한다.
또한 멤버는 하나의 팀만 가지니 팀 객체를 하나만 가지는 필드를 선언했다면
팀은 여러 멤버를 가지는 리스트 필드를 선언한다.
일대다 컬렉션 조회
Team team = em.find(Team.class, 1);
List<Member> members = team.getMembers();
일대다 컬렉션도 마찬가지로 객체 그래프 탐색을 이용해서 조회한다.
연관관계의 주인
데이터베이스에서는 외래 키 하나 만으로 양방향 관계를 맺을 수 있지만
객체는 두 개의 단방향 관계를 사용하여 양방향 관계를 흉내내는 것만 가능하다.
하지만 객체를 양방향으로 만들면 참조는 둘인데 외래 키가 하나인 경우가 되기에
둘 사이에 차이가 발생하는 문제가 생긴다.
이를 해결하기 위해 두 객체의 연관관계 중 하나를 정해서
테이블의 연관관계를 관리하게 하고 이러한 역할을 연관관계의 주인이라 한다.
양방향 매핑 규칙
양방향 매핑을 하는 경우 두 연관관계 중 하나를 무조건 연관관계의 주인으로 정해야 한다.
연관관계의 주인만 데이터베이스 연관관계와 매핑되어 외래 키를 관리할 수 있게 만들고
나머지 반대 쪽은 읽기만 할 수 있게 만들어 위에서 언급한 차이를 없앤다.
public class Member {
// 기존 코드 생략
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
public class Team {
// 기존 코드 생략
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
}
위의 코드처럼 연관관계의 주인인 Member 엔티티의 필드에는
mappedBy 속성을 사용하지 않고
주인이 아닌 Team 엔티티의 필드에는
mappedBy 속성을 사용하여 연관관계의 주인을 Member 엔티티의 team 필드로 지정한다.
즉, 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 지정해야 한다.
멤버는 팀을 옮길 수 있으니 수정권한이 필요하지만
팀은 멤버를 옮길 수 없으니 수정권한이 필요 없다고 볼 수 있다.
다대일과 일대다 관계에서는 항상 다 쪽이 외래키를 가진다.
양방향 연관관계 저장
Team team1 = new Team(1, "팀1");
em.persist(team1);
Member member1 = new Member(1, "홍길동");
member1.setTeam(team1);
em.persist(member1);
위의 코드를 보면 단방향 연관관계에서의 코드와 완전히 일치하지만
이 코드가 양방향 연관관계를 저장하는 방식이다.
연관관계의 주인만 연관관계의 작업이 가능하기에
팀 엔티티가 아닌 멤버 엔티티를 통해 작업하는 것이다.
양방향 연관관계의 주의점
Member member1 = new Member(1, "홍길동");
em.persist(member1);
Team team1 = new Team(1, "팀1");
team1.getMembers().add(member1);
em.persist(team1);
위의 코드처럼 연관관계의 주인이 아닌 곳에서 값을 입력하는 경우에는
읽기전용이기 때문에 값이 추가되지 않는다.
public class Member {
// 기존 코드 생략
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
}
public class Team {
// 기존 코드 생략
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
}
객체까지 고려해서 주인이 아닌 곳에도 값을 입력하는 것이 좋고,
객체의 양방향 연관관계는 반드시 양쪽 모두 관계를 맺어줘야 한다.
연관관계 편의 메서드
기존의 연관관계의 주인인 쪽의 Setter 메서드는 자신 쪽의 객체의 값만 수정하여
반대 쪽의 객체에는 값이 입력되지 않는다.
이러한 문제를 해결하기 위해 Setter를 수정하는데
이렇게 양쪽에 값을 추가하게 수정된 Setter를 편의 메서드라고 부른다.
수정된 코드는 아래와 같다.
public void SetTeam(Team team) {
// 기존의 팀이 있다면 연관관계를 제거
if(this.team != null) {
this.team.getMembers().remove(this);
}
// 주인뿐만 아니라 반대 쪽의 객체에도 값을 추가
this.team = team;
team.getMembers().add(this);
}
기존에 어떤 팀과 연관관계가 존재한다면 연관관계를 끊고
다른 팀과 연결해야하니 연관관계가 존재하는지 확인 후에
주인 쪽 객체에만 값을 추가하는 것이 아니라 양쪽 모두에 값을 추가한다.
정리
단방향 매핑만으로도 테이블과 객체의 연관관계 매핑은 완료되었기에
무조건 양방향 매핑을 할 필요는 없다.
양방향 매핑은 그저 반대 쪽에서도 객체 그래프 탐색 기능을
사용할 수 있다는 점을 빼면 장점이 없기 때문에
상황을 봐가면서 추가 하는 것이 좋다.
'Back-End > JPA' 카테고리의 다른 글
다양한 연관관계 매핑 (0) | 2023.06.21 |
---|---|
엔티티 매핑 : 학습을 위한 스키마 자동 생성하기 (0) | 2023.06.19 |
엔티티 매핑 : 매핑 어노테이션 (0) | 2023.06.19 |
영속성과 엔티티 (0) | 2023.06.01 |
JPA 시작하기 (0) | 2023.06.01 |