5장. 연관관계 매핑 기초

배유연·2023년 10월 18일

객체의 참조와 테이블의 외래키를 매핑하는 것이 이 장의 목표이다!!

키워드

방향 : [단방향, 양방향]이 있다. 방향은 객체관계에만 존재하고, 테이블관계는 항상 양방향이다.
다중성 : [다대일, 일대다, 일대일, 다대다]
연관관계의 주인 : 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야한다.

1. 단방향 연관관계

그림을 먼저 보자.

객체 연관관계

  • 회원객체는 Member.team 필드로 팀 객체와 연관관계를 맺는다.
  • 회원객체와 팀 객체는 단방향 관계다.

테이블 연관관계

  • 회원 테이블은 TEAM_ID 외래키로 팀 테이블과 연관관계를 맺는다.
  • 회원 테이블과 팀 테이블은 양방향 관계다.

객체 연관관계와 테이블 연관관계의 가장 큰 차이

  • 참조를 통한 연관관계는 언제나 단방향이다.
  • 테이블은 외래키 하나로 양방향 조인을 할 수 있다.

객체 관계 매핑

Member.team과 MEMBER.TEAM_ID를 매핑하는 것이 연관관계 매핑이다.

@Entity
public class Member {

    ...
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    ...
}
  • @ManyToOne : 다대일 관계라는 매핑 정보다.
  • @JoinColumn(name="TEAM_ID") : 조인 컬럼은 외래키를 매핑할 때 사용한다.
    TEAM_ID 외래키로 연관관계를 맺는다.
    • @JoinColumn 을 생략하면 필드명_참조하는 테이블의 컬럼명이 외래키가 된다.
      (team_TEAM_ID)

@JoinColumn 의 주요 속성

속성기능기본값
name매핑할 외래키 이름필드명+_+참조하는 테이블의 기본키 컬럼명
referencedColumnName외래키가 참조하는 대상 테이블의 컬럼명참조하는 테이블의 기본키 컬럼명
foriegnKey(DDL)외래 키 제약조건을 직접 설정할 수 있다.
unique, nullable, insertable, updatable, columnDefinitaion, table@Column 의 속성과 같다.

@ManyToOne 속성

속성기능기본값
optionalfalse 로 설정하면 연관된 엔티티가 항상 있어야함.true
fetch글로벌 페치 전략 설정@ManyToOne=FetchType.EAGER @OneToMany=FetchType.LAZY
casecade영속성 전이 기능

2. 연관관계 사용

✔ 등록

	private static void testSave(EntityManager em) {
        Team team1 = new Team("team1", "팀1");
        em.persist(team1);

        //회원1 저장
        Member member1 = new Member("member1", "회원1");
        member1.setTeam(team1);
        em.persist(member1);

        //회원2 저장
        Member member2 = new Member("member2", "회원2");
        member2.setTeam(team1);
        em.persist(member2);

    }

JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속상태여야한다.

위 코드를 실행하면, MEMBER 데이터를 저장할 때 외래키로 'team1'이 저장되는 것을 확인 할 수 있다.

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) 객체 지향 쿼리 사용

객체 그래프 탐색

Member member = em.find(Member.class,"member1");
Team team =member.getTeam();//객체 그래프 탐색

member.getTeam() 처럼 객체를 통해 연관된 엔티티를 조회하는 것을 말한다.

객체지향 쿼리사용(JPQL)
코드를 먼저 보자.

private static void queryLogicJoin(EntityManager em) {
	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("member 명= " + member.getUsername());
	}
}

JQPL 문을 보면 일반 SQL과 살짝 다르다.
JQPL은 객체 대상으로 하고 SQL은 테이블 대상으로한다.
참고로 :teamName과 같이 :로 시작하는 것은 파라미터를 바인딩 받는 문법이다.

✔ 수정

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

얘도 em.update() 같은 메소드는 없다. 단순히 엔티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동한다.

✔ 연관관계 제거

 private static void deleteRelation(EntityManager em) {
    Member member1 = em.find(Member.class, "member1");
    member1.setTeam(null);
}

이것을 실행하면 다음과 같은 UPDATE 구문이 실행된다.

UPDATE MEMBER
SET TEAM_ID = null
WHERE ID='member1';

✔ 연관된 엔티티 삭제

연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야한다.
그 이유는? 외래키 제약조건으로 인해 DB오류가 발생한다.

    member1.setTeam(null);
    em.remove(team);

3. 양방향 연관관계

회원과 팀이 양방향으로 접근할 수 있도록, Team.members 를 List 형태로 추가해보자.
객체 연관관계

테이블 연관관계
테이블 연관관계는 아까 단방향 연관관계와 다를게 없다.
데이터베이스 테이블은 외래키 하나로 양방향 조회가 가능하기 때문이다.

양방향 연관관계 매핑

Member 코드는 변경할 게 없고, Team 코드를 변경해야한다.

@Entity
public class Team {

    @Id
    @Column(name = "TEAM_ID")
    private String id;

    private String name;

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

일대다 관계를 매핑하기 위해 @OneToMany 매핑정보를 사용했다.
mappedBy 속성은 양방향 매핑일 때 사용하는데, 반대쪽 매핑의 필드이름을 값으로 설정하면 된다.

4. 연관관계의 주인

연관관계의 주인이란?
JPA에서 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리하는 객체이다.
연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래키를 관리할 수 있다.
즉, mappedBy 속성이 없는 쪽이 주인이다!
mappedBy를 갖고있는 객체는 읽기만 가능하고, 외래키를 변경하지 못한다.

참고
데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래키를 가져서
@ManyToOne 에는 mappedBy 속성이 없다.

5. 양방향 연관관계 저장

저장 코드는 단방향 연관관계의 저장하는 코드와 완전히 같다!!
이유는, 연관관계의 주인인 Member만 member.setTeam(team1); 만 세팅하면 되기 때문이다.

6. 양방향 연관관계의 주의점

양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌곳에만 값을 입력하는 것이다.
다음 코드를 보자.

public static void testSaveNonOwner(EntityManager em) {
	//회원1 저장
    Member member1 = new Member("member1", "회원1");
    em.persist(member1);

    //회원2 저장
    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 에만 값을 저장하면, 실제로 MEMBER 테이블에 null로 데이터가 저장된다

순수한 객체까지 고려한 양방향 연관관계

그러면, 주인에게만 값을 저장하면 될까?
사실은 객체 관점에서 양쪽 방향 모두 값을 입력해주는 것이 가장 안전하다.
이를 위해 Member 클래스의 setTeam()메소드를 리팩토링 해보자.

public void setTeam(Team team) {

     //기존 팀과 관계를 제거
     if (this.team != null) {
     	this.team.getMembers().remove(this);
	 }
        
     this.team = team;
     team.getMembers().add(this);
}

위 코드 중 this.team.getMembers().remove(this); 작업을 한 이유는
다음과 같은 상황 때문이다.

member1.setTeam(teamA);
member1.setTeam(teamB);

List<Member> findMembers = teamA.getMembers();

를 하면 원하지 않은 member1이 조회될 것이다. 따라서 기존에 Member에 저장되어있던 team을 제거해주는 처리를 해줘야한다.
team.getMembers().add(this); 처리를 추가하므로써 setTeam() 메소드 하나로만 양방향 관계 설정이 완료되었다.

🤚양방향의 장점은?

양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것 뿐이다.

profile
기초를 중요시하는 백엔드개발자입니당

0개의 댓글