안녕하세요. 오늘은 jpa 연관관계 매핑 기초에 대해 알아보려 합니다.
김영한님의 자바 ORM 표준 JPA 프로그래밍을 읽고 정리했습니다.
단방향 연관관계
연관관계 중에서는 다대일 단방향 관계를 가장 먼저 이해해야한다.
회원과 팀의 관계를 통해 다대일 관계를 알아보려고 한다.
- 회원과 팀이 있다.
- 회원은 하나의 팀에만 속할 수 있다.
- 회원과 팀은 다대일 관계다.
테이블 연관관계의 경우 TEAM_ID를 외래 키로 사용하여 회원 테이블과 팀 테이블이 연관관계를 맺는다.
그렇게 되면 두 테이블은 양방향 관계가 되고 회원이 팀을 조회할 수 있고, 팀도 회원을 조회할 수 있다.
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID; # 회원과 팀을 조인
SELECT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID; # 팀과 회원을 조인
객체의 경우 회원 객체와 팀 객체는 단방향 관계로 회원은 Member.team으로 팀을 알 수 있지만 Team은 member를 알 수 없다.
참조를 통한 연관관계 설정 시 언제나 단방향이며 양방향으로 만들고 싶으면 Team 객체에 필드를 추가하여 참조를 보관해야한다. 하지만 이 또한 양방향 관계보다는 서로 다른 단방향 매핑 2개이다.
JPA를 사용하여 객체를 매핑한 코드이다.
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
// 연관관계 매핑
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// 연관관계 설정
public void setTeam(Team team) {
this.team = team;
}
}
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
}
@ManyToOne
다대일 관계라는 매핑 정보이며 연관관계를 매핑할 때는 이렇게 다중성을 나타내는 어노테이션을 필수도 사용해야한다.
@JoinColumn
조인 컬럼은 외래 키를 매핑할 때 사용한다. name 속성에는 매핑할 외래 키 이름을 지정한다. 회원과 팀 테이블은 TEAM_ID를 외래 키로 연관관계를 맺으므로 이 값을 지정하면 된다. 이 어노테이션은 생략 가능하다.
연관관계 사용
저장
연관관계를 이용하여 엔티티를 어떻게 저장하는 지 알아보자.
public void testSave() {
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); // 회원 -> 팀 참조
em.persist(member1); // 저장
Member member2 = new Member("member2", "회원2");
member1.setTeam(team1);
em.persist(member2);
}
아래와 같이 SQL이 실행된다.
INSERT INTO TEAM (TEAM_ID, NAME) VALUES ('team1', '팀1');
INSERT INTO MEMBER (MEMBER_ID, NAME, TEAM_ID) VALUES ('member1', '회원1', 'team1');
INSERT INTO MEMBER (MEMBER_ID, NAME, TEAM_ID) VALUES ('member2', '회원2', 'team1');
조회
연관관계가 있는 엔티티를 조회하는 방법은 2가지가 있다.
1. 객체 그래프 탐색
2. 객체지향 쿼리 사용 (jpql)
객체 그래프를 이용해서 탐색하면 member와 연관된 team 엔티티를 조회할 수 있다.
Member member = em.find(Member.class, "member1");
Team team = member.getTeam();
System.out.println("팀 이름 : " + team.getName());
jpql의 from Memberm join m.team t 부분은 회원이 팀과 관계를 갖고 있는 m.team 필드를 통해서 member와 team을 조인했다. 그리고 where 절을 통해서 t.name 을 검색 조건을 이용하여 팀 이름이 팀1에 속한 회원만 검색했다.
private static void queryLogicJoin(EntityManager em) {
String jpql1 = "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();
for (Member member : resultList) {
System.out.println("[query] member.username : " + member.getUsername());
}
}
수정
영속성 관리 부분에서 설명했듯이 jpa에는 em.update() 메서드가 없다. 대신 변경 감지 기능이 있어서 트랜잭션이 커밋되고 플래시가 작동되면 스냅샷과 엔티티를 비교해서 수정 쿼리를 생성한다. 그리고 이를 데이터베이스에 반영한다.
private static void updateRelation(EntityManager em) {
Team team2 = new Team("team2", "팀2");
em.persist(team2);
Member member = em.find(Member.class, "member1");
member.setTeam(team2);
}
제거
연관관계를 null로 설정하면 연관관계가 제거된다.
private static void deleteRelation(EntityManager em) {
Member member1 = em.find(Member.class, "member1");
member1.setTeam(null);
}
엔티티를 삭제할 때도 기존의 연관관계를 모두 제거한 후 삭제해야한다. 그렇지 않으면 외래 키 제약조건으로 데이터베이스 오류가 발생한다.
양방향 연관관계
이제껏 회원에서 팀을 참조하는 관계를 설정했다면 이번에는 반대방향인 팀에서 회원으로 접근하는 필드를 추가해보려한다.
회원과 팀은 다대일 관계였다면 팀과 회원은 반대로 일대다 관계이다.
일대다 관계는 여러 건과 연관관계를 맺으므로 컬렉션을 사용해야한다. Team.members는 List 컬렉션을 추가했다.
데이터베이스에서는 원래 양방향으로 조회 가능하므로 추가할 내용이 없다.
아래는 jpa를 사용해서 양방향 매핑을 한 코드이다.
멤버 엔티티는 변화가 없다.
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
// 연관관계 매핑
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// 연관관계 설정
public void setTeam(Team team) {
this.team = team;
}
}
팀 엔티티에는 members 필드를 추가했다. 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용했다. mappedBy 속성은 양방향 매핑일 때 사용하는데 매핑의 필드 이름을 값으로 준다.
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
}
아래는 컬렉션을 이용하여 조회하는 코드이다.
public void biDirection() {
Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers();
for (Member member : members) {
System.out.println("member username : "+ member.getUsername);
}
}
연관관계 주인
@OneToMany만 있으면 되지 왜 mappedBy 속성이 있어야할까?
사실 객체에는 양방향 연관관계라는 것이 없다. 2개의 단방향 관계를 어플리케이션 로직으로 묶어서 양방향인 것처럼 보이게 하는 것이다.
엔티티를 양방향 연관관계로 설정하면 객체의 참조는 두 개인데 외래 키는 하나이다. 따라서 두 연관관계 중 하나를 정하여 테이블의 외래 키를 관리해야 하는데 이를 연관관계 주인이라고 한다.
연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다. 회원과 팀 관계에서는 회원이 테이블의 외래 키를 갖고 있으므로 Member.team이 주인이 된다.
연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다. 주인이 아닌 쪽은 읽기만 할 수 있다. 주인이 아닌 쪽만 mappedBy 속성을 사용하면 주인은 사용하지 않는다.
양방향 연관관계 저장
팀1을 저장하고 회원1, 회원2에 연관관계의 주인인 Member.team 필드를 통해 회원과 팀의 연관관계를 설정하고 있다. 이 코드는 단방향 연관관계와 완전히 동일하다.
public void testSave() {
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1);
em.persist(member1);
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1);
em.persist(member2);
}
주인이 아닌 곳에 입력된 값은 외래 키에 영향을 주지 않으며 아래의 코드는 데이터베이스에 저장할 때 무시한다.
team1.getMembers().add(member1);
team1.getMembers().add(member2);
엔티티 매니저는 연관관계의 주인인 곳에서 입력된 값으로 외래 키를 관리한다.
member1.setTeam(team1);
member2.setTeam(team1);
양방향 연관관계의 주의점
양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다.
public void testSave() {
Member member1 = new Member("member1", "회원1");
em.persist(member1);
Member member2 = new Member("member2", "회원2");
em.persist(member2);
Team team1 = new Team("team1", "팀1");
// 연관관계 주인이 아닌 곳에만 연관관계 설정
team1.getMembers().add(member1);
team1.getMembers().add(member2);
em.persist(team1);
}
이렇게 되면 TEAM_ID에는 null 값이 입력된다.
그렇다고 연관관계 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 저장하지 않아도 된다는 것은 아니다.
객체 관점에서는 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
이렇게 하면 우리가 기대하는 양방향 연관관계의 결과가 나온다.
public void testSave() {
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
// 양방향 연관관계 설정
member1.setTeam(team1);
team1.getMembers().add(member1);
em.persist(member1);
Member member2 = new Member("member2", "회원2");
// 양방향 연관관계 설정
member2.setTeam(team1);
team1.getMembers().add(member2);
em.persist(member2);
}
양방향 연관관계는 양쪽을 모두 신경 써야하고 이렇게 하다보면 둘 중 하나를 사용하지 않는 실수를 할 수 있으므로 코드를 하나인 것처럼 사용하는 것이 좋다.
public class Member {
private Team team;
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
이 메서드에는 버그가 있는데 team1에서 team2로 변경할 때 team1과의 관계가 남아있다.
이를 제거해주는 부분도 필요하다.
public class Member {
private Team team;
public void setTeam(Team team) {
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
}
'🍀spring > 스프링 jpa' 카테고리의 다른 글
[Spring JPA] 프록시와 연관관계 관리 (0) | 2024.03.03 |
---|---|
[Spring JPA] 고급 매핑 (상속, 복합 키 매핑) (0) | 2024.02.29 |
[Spring JPA] 다양한 연관관계 매핑 (1) | 2024.02.29 |
[Spring JPA] 엔티티 매핑 (1) | 2024.02.26 |
[Spring JPA] 영속성 관리 (2) | 2024.02.25 |