[JPA] 연관관계 매핑 기초

Updated:

들어가며

해당 글은 자바 ORM 표준 프로그래밍을 정리한 글입니다.

Entity 간에 연관관계에 대해서 간단하게 알아 본다. 기초에서는 단방향 연관관계와 양방향 연관관계에 대해서 설명하고 연관관계 매핑시에 주의해야 하는 부분을 알아보자.

단방향 연관관계

  • 참조를 통한 연관관계는 언제나 단방향으로만 설정 된다.
  • 객체는 참조(주소)로 연관관계를 맺는다.
  • 테이블은 외래 키로 연관관계를 맺는다.
  • 객체 그래프 탐색은 연관관계를 맺은 엔티티를 참조해서 해당 엔티티를 탐색하는 걸 의미한다.
    • member.getTeam() 객체 참조를 사용해서 연관관계를 탐색
@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    ...
    // 단방향 연관관계 설정
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
    ...
}

@Entity
public class Team {
    @Id
    @GeneratedValue
    @Column(name = "team_id")
    private Long id;
    ...
}

@JoinColumn

  • 외래 키를 매핑할 때 사용하며, 생략 가능하다.
  • @JoinColumn을 사용하지 않을 경우 필드명 + ‘_’ + 참조하는 테이블의 기본키 컬럼명으로 외래키가 자동 생성 된다.
    alter table member 
       add constraint FKa04a95gj3stpfpgfofmg3f694 
       foreign key (team_team_id) 
       references team

@ManyToOne

  • 다대일(N:1) 관계일때 사용
  • 여러 멤버가 한 팀에 속할 수 있다고 가정한다면, 여러 멤버는 한 팀에 속할 수 있으므로 멤버쪽이 N이되고 팀쪽이 1이라 볼 수 있다.
속성 기능 기본값
optional false로 설정하면 연관된 엔티티가 항상 있어야 한다. true
fetch 글로벌 패치 전략을 설정 @ManyToOne=FetchType.EAGER @OneToMany=FetchType.LAZY
cascade 영속성 전이 기능을 사용한다. {}
targetEntity 연관된 엔티티의 타입 정보를 설정 void.class

연관관계 사용

저장

  • JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
  • 영속상태가 아닌 엔티티를 참조해서 저장하려고 하는 경우 에러가 발생한다.

코드

@DataJpaTest
class testSave {
    @Autowired
    TestEntityManager em;

    @Test
    void testSave() {

        Team team = new Team();
        em.persist(team); // 해당 부분을 주석으로 하는 경우 에러 발

        Member member = new Member();
        member.setTeam(team);
        em.persist(member);

        em.flush(); // flush를 해줘야 insert query가 실행 된다.
    }
}
insert 
    into
        team
        (team_id) 
    values
        (?)
insert 
    into
        member
        (team_team_id, member_id) 
    values
        (?, ?)
        

조회

  • 객체 그래프 탐색

코드

Member member = em.find(Member.class, 1);
Team team = member.getTeam(); // 객체 그래프 탐색
  • 객체지향 쿼리 사용(JPQL)

코드

List<Member> resultList = em.getEntityManager()
                            .createQuery(jqpl, Member.class)
                            .setParameter("teamName", "팀1")
                            .getResultList();

실행 결과

    select
        member0_.member_id as member_i1_0_,
        member0_.string_status as string_s2_0_,
        member0_.team_team_id as team_tea3_0_ 
    from
        member member0_ 
    inner join
        team team1_ 
            on member0_.team_team_id=team1_.team_id 
    where
        team1_.name=?

연관된 엔티티 삭제

  • 연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다. 그렇지 않으면 외래 키 제약조건으로 인해, 데이터베이스에서 오류가 발생

양방향 연관관계

@Entity
public class Team {
    @Id
    @GeneratedValue
    @Column(name = "team_id")
    private Long id;

    //==추가==//
    @OneToMany(mappedBy="team")
    private List<Member> members = new ArrayList<Member>();
}

연관관계의 주인

  • 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 하는데 이것을 연관관계의 주인(Owner)이라 한다.
  • 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
  • mappedBy 속성을 사용해 연관관계의 주인을 결정
    • 주인은 mappedBy 속성을 사용하지 않는다.
    • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
  • 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다.

양방향 연관관계의 주의점

코드

@Test
void 문제확인() {
  // 회원 1 저장
  Member member1 = new Member();
  em.persist(member1);
  // 회원 2 저장
  Member member2 = new Member();
  em.persist(member2);

  Team team = new Team();
  // 주인이 아닌 곳만 연관관계 설정
  team.getMembers().add(member1);
  team.getMembers().add(member2);
  
  em.clear(); // 영속성 컨텍스트 초기화
  
  Member selectedMember = em.find(Member.class, member1.getId());
  System.out.println("team : " + selectedMember.getTeam());
}

결과

team : null
  • 연관관계에 주인이 되는 member에 team의 외래키를 설정하지 않았기 때문에 team 관련 정보가 null로 insert 된다.
  • 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.

수정된 코드

@Test
void 문제확인() {
  
  Team team = new Team();
  em.persistAndFlush(team);

  // 회원 1 저장
  Member member1 = new Member();
  member1.setTeam(team);
  team.getMembers().add(member1);
  em.persistAndFlush(member1);

  // 회원 2 저장
  Member member2 = new Member();
  member2.setTeam(team);
  team.getMembers().add(member2);
  em.persistAndFlush(member2);

  em.clear(); // 영속성 컨텍스트 초기

  Member selectedMember = em.find(Member.class, member1.getId());
  System.out.println("team : " + selectedMember.getTeam());

}

결과

team : me.blog.jpa.model.Team@1ab9c735

마치며

엔티티 간에 단방향 연관관계와 양방향 연관관계에 대해서 간단하게 알아 보았다. 단방향 연관관계를 사용할 때는 많이 신경 쓸 부분이 없었지만 양방향 연관관계를 사용하는 경우 연관관계의 주인이 되는 엔티티에 외래키로 사용 될 참조 객체를 포함하고 있는지에 대해서 확인해주어야 한다.

cascade 옵션을 사용하게 되면 해결을 할 수 있지만 양방향 연관관계시에 참조가 어떤 식으로 이뤄지는지 문제점을 보고 이해 하고 넘어가면 좋을 것 같다.👍



Leave a comment