본문 바로가기
JPA/자바 ORM 표준 JPA 프로그래밍 - 기본편

[JPA] 단방향 연관관계

by hk27 2022. 2. 4.
객체 연관관계와 테이블 연관관계를 매핑해봅시다.
객체의 참조와 테이블의 외래 키 매핑하는 것입니다. 

 

안녕하세요.

단방향 연관관계에 대해서 알아보겠습니다.

 

JPA는 객체와 관계형 DB 두 기둥 위에 있는 기술이라고 말합니다.

객체와 관계형 DB를 이어주는 기술이기 때문입니다.

연관관계는 객체와 관계형 DB 모두에서 정말 중요합니다.

그러나 둘은 다릅니다. 아래 사진을 봅시다. 

 

객체와 테이블의 연관관계

 

객체는 참조(주소)를 사용해서 관계를 맺습니다. 회원 객체는 Member.team 필드로 팀 객체와 연관관계를 맺습니다.

또한, 회원 객체와 팀 객체는 단방향 관계입니다. 단방향 관계는 회원 -> 팀, 팀 -> 회원 둘 중 한쪽만 참조하는 것을 의미합니다. 여기서는 회원 -> 팀으로 회원만 팀을 참조하고, 팀은 회원을 참조하지 않습니다. 

 

테이블은 외래 키를 사용해서 관계를 맺습니다. 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺습니다.

회원 테이블과 팀 테이블은 양방향 관계입니다. 양방향 관계는 회원 -> 팀, 팀 -> 회원 양쪽 모두 서로를 참조하는 것을 의미합니다. 두 테이블은 join을 통해 서로를 참조할 수 있습니다. 

 

JPA는 객체 연관관계와 테이블 연관관계를 매핑하기 위해 객체의 참조와 테이블의 외래 키를 매핑합니다. 

그렇다면 객체의 단방향 관계와 테이블의 양방향 관계는 어떻게 매핑할까요?

JPA는 객체의 단방향 연관관계 만으로 테이블의 양방향 연관관계를 매핑합니다.

예를 들어서 위의 그림에서 팀 객체는 멤버 객체를 참조하지 않아도, 팀 테이블은 멤버 테이블과 조인해서 참조할 수 있습니다. 

또는 객체도 테이블처럼 양방향 연관관계를 가질 수도 있습니다. 

이번 게시글에서는 단방향 연관관계를 알아보고, 다음 게시글에서 양방향 연관관계를 알아보겠습니다.

 

연관관계 매핑

객체의 참조기반 단방향 연관관계와 테이블의 외래 키 기반 양방향 연관관계를 매핑해봅시다. 

가장 중요한 것은 Member 객체의 team 참조를 MEMBER 테이블의 TEAM_ID 외래 키에 매핑하는 것입니다. 

 

멤버 엔티티

@Getter @Setter
@Entity
public class Member {
    @Id
    @Column(name="MEMBER_ID")
    private String id;
    private String username;

    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;

    public Member() {}

    public Member(String id, String username) {
        this.id = id;
        this.username = username;
    }
}

 

팀 엔티티

@Getter @Setter
@Entity
public class Team {
    @Id
    @Column(name="TEAM_ID")
    private String id;
    private String name;
    
    public Team() {}
    
    public Team(String id, String name) {
        this.id = id;
        this.name = name;
    }
}

 

가장 중요한 것은 멤버 엔티티 9~11번째 줄의 코드입니다. 

@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;

@ManyToOne은 이름 그대로 다대일(N:1) 관계라는 매핑 정보입니다. 멤버 엔티티 입장에서 팀과 N:1 관계입니다.

연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해서 JPA에 두 엔티티의 관계를 알려야 합니다. 

@JoinColumn은 객체와 외래 키를 매핑하는 어노테이션입니다. team 객체를 TEAM_ID라는 외래 키로 매핑합니다.

JPA가 알아서 team 객체의 id 값을 저장합니다.

JoinColumn은 단어에서부터 조인하는 컬럼, 즉 외래 키라는 의미를 나타냅니다. 

 

연관관계 저장

연관관계를 매핑한 엔티티를 어떻게 저장하는지 알아봅시다. 

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);

 

member1.setTeam(team1)을 해서 회원 엔티티는 팀 엔티티를 참조합니다.

이후 em.persist(member1)를 해서 저장하면 JPA는 팀의 식별자(Team.id)를 회원 테이블의 외래 키값으로 입력합니다.

DB에 값이 잘 저장된 것을 확인할 수 있습니다. 

 

주의할 점은, JPA에서 엔티티를 저장할 때 모든 연관된 엔티티는 영속 상태여야 합니다. 

팀 객체를 먼저 persist하고, 멤버에 set 해야 합니다.

만약 먼저 persist하지 않으면 에러가 납니다. 

TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : hellojpa.start.Member.team -> hellojpa.start.Team]

'개체가 저장되지 않은 임시 인스턴스를 참조합니다. 플러시하기 전에 임시 인스턴스를 저장하세요.'라는 에러 메시지가 뜹니다. transient instance는 비영속(영속 상태에 들어가지 않은) 객체를 의미합니다. team 객체가 비영속 객체이기 때문에 에러가 뜬 것입니다. 

아래 글은 영속성에 대해 작성한 게시글입니다. 관심 있으신 분은 아래 게시글을 참고해주세요.

[JPA] 영속성 관리

 

조회

연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지입니다.

객체 그래프 탐색, 객체지향 쿼리 사용입니다. 한 개씩 알아봅시다.

 

1. 객체 그래프 탐색

객체 연관관계를 사용한 조회를 의미합니다.

객체를 DB에서 찾아서 getTeam으로 연관된 엔티티를 조회할 수 있습니다. 

Member member = em.find(Member.class, "member1"); 
Team team = member.getTeam();
System.out.println("팀 이름 = " + team.getName()); // 팀 이름 = 팀1

 

2. 객체 지향 쿼리 사용

객체 지향 쿼리인 JPQL로도 연관관계가 있는 엔티티를 조회할 수 있습니다.

예를 들어서 팀1에 소속된 회원을 조회하려면 조인해서 검색합니다. 

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();
                            
for (Member member : resultList) {
    System.out.println("[Query] member.username= " + member.getUsername());
    // [Query] member.username= 회원1
    // [Query] member.username= 회원2
}

 

수정 

다음으로 연관관계를 어떻게 수정하는지 알아봅시다.

member.setTeam(team2)으로 객체의 참조 대상을 변경하면 DB에 자동으로 반영됩니다.

Team team2 = new Team("team2", "팀2");
em.persist(team2);

Member member = em.find(Member.class, "member1");
member.setTeam(team2);

 

member1의 TEAM_ID가 team2로 잘 변경된 것을 확인할 수 있습니다. 

 

연관관계 제거

연관관계를 null로 설정하면 됩니다.

Member member = em.find(Member.class, "member1");
member.setTeam(null); // 연관관계 제거

 

연관된 엔티티 삭제

연관된 엔티티를 삭제하려면 기존의 연관관계를 먼저 제거해야 합니다. 그렇지 않으면 외래 키 제약조건으로 인해 오류가 발생합니다. 

예를 들어서 팀1을 삭제하고 싶은데 회원1이 소속되어있다면 에러가 납니다.

ERROR: Referential integrity constraint violation: "FKL7WSNY760HJY6X19KQNDUASBM: PUBLIC.MEMBER FOREIGN KEY(TEAM_ID) REFERENCES PUBLIC.TEAM(TEAM_ID) ('team1')"; 

 

먼저 연관관계를 제거하고 팀을 삭제해야 합니다. 

아래 코드처럼 작성하면 됩니다. 

Team team = em.find(Team.class, "team1");
Member member = em.find(Member.class, "member1");
member.setTeam(null);
em.remove(team);

 

 

이번 게시글에서는 단방향 연관관계를 어떻게 매핑하고, CRUD 코드를 어떻게 작성하는지 알아보았습니다.

다음 게시글에서는 양방향 연관관계와 연관관계의 주인을 알아보겠습니다.

이어지는 내용이니 관심 있으신 분은 아래 게시글을 참고해주세요. 

[JPA] 양방향 연관관계와 연관관계의 주인

 

 

인프런  '자바 ORM 표준 JPA 프로그래밍 - 기본편' 강의를 듣고 공부하며 정리한 자료입니다. 

잘못된 부분은 피드백 주시면 감사하겠습니다. 

글 읽어주셔서 감사합니다 :-)

 

참고 자료

자바 ORM 표준 JPA 프로그래밍 - 기본편 섹션 5. 연관관계 매핑 기초, https://www.inflearn.com/course/ORM-JPA-Basic

김영한, 자바 ORM 표준 JPA 프로그래밍, 에이콘출판(2015), pp.164-178.

 

댓글