연관관계 매핑 기초

5. 연관관계 매핑 기초

  • 연관관계 매핑
    • 엔티티는 다른 엔티티와 연관관계를 형성한다.
    • 객체는 참조를 사용하여 다른 객체와 관계를 형성한다.
    • 데이터베이스 테이블은 외래 키를 사용하여 다른 테이블과 관계를 형성한다.
    • 연관관계 매핑이란 객체의 참조와 테이블의 외래 키를 매핑하는 것을 말한다.
  • 방향
    • 단방향: 회원->팀 또는 팀->회원 처럼 한 쪽만 참조하는 관계를 말한다.
    • 양방향: 회원->팀, 팀->회원 양쪽 모두 참조하는 관계를 말한다. 테이블 관계는 항상 양방향이다.
  • 다중성
    • 다대일(N:1)
    • 일대다(1:N)
    • 일대일(1:1)
    • 다대다(N:N)
  • 연관관계의 주인
    • 객체를 양방향 연관관계로 만들 때 연관관계의 주인을 정해야한다.

5.1 단방향 연관관계

  • 회원과 팀의 관계는 다대일(N:1) 단방향 관계이다.
  • 객체 연관관계
    • 객체는 Member.team 필드로 연관관계를 가진다.
    • 회원 객체와 팀 객체는 단방향 관계이다. 회원은 팀에 접근가능하지만 팀은 회원에 접근할 수 없다.
  • 테이블 연관관계
    • 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 가진다.
    • 회원 테이블과 팀 테이블은 양방향 관계이다.
    • TEAM_ID 외래 키를 통해 회원과 팀을 조인할 수 있고 반대로 팀과 회원을 조인할 수 있다.
-- MEMBER 와 TEAM 조인
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
-- TEAM 과 MEMBER 조인
SELECT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
  • 객체 연관관계와 테이블 연관관계의 차이
    • 참조를 통한 연관관계는 단방향이다.
    • 객체의 양방향관계를 만들려면 반대쪽 객체에 필드를 추가해서 참조해야한다. 정확히 표현하면 객체의 양방향관계는 서로 다른 2개의 단방향관계이다.
    • 테이블은 외래 키를 통해 양방향으로 조인이 가능하다.
  • 정리
    • 객체는 참조로 연관관계를 맺는다.
      • 참조는 단방향이다.
      • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
    • 테이블은 외래 키로 연관관계를 맺는다.
      • 외래 키는 양방향으로 조인이 가능하다.

5.1.3 객체 관계 매핑

  • 객체의 Member.team 필드와 테이블의 MEMBER.TEAM_ID 외래 키를 매핑하는 것이 연관관계 매핑이다.
  • @ManyToOne은 다대일(N:1) 관계 매핑 정보를 나타낸다.
  • @JoinColumn은 외래 키를 매핑한다. name 속성에 매핑 할 외래 키 이름을 지정한다.
@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String username;

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

    // getter, setter 생략
}
@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name = "team_id")
    private Long id;

    private String name;

    // getter, setter 생략
}
  • @JoinColumn
속성 기능 기본값
name 매핑할 왜래 키 이름 필드명 + ‘_’ + 참조하는 테이블의 기본 키 컬럼명
referencedColumnName 외래 키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 기본 키 컬럼명
foreignKey 외래 키 제약조건을 직접 지정할 수 있다.  
unique @Table의 uniqueConstraints와 같지만 한 컬럼에 간단히 유니크 제약조건을 걸 때 사용한다. 만약 두 컬럼 이상을 사용해서 유니크 제약조건을 사용하려면 클래스 레벨에서 @Table.uniqueConstraints를 사용해야 한다.  
nullable null 값의 허용 여부를 설정한다. true
insertable 엔티티 저장 시 이 필드도 같이 저장한다. false 옵션은 읽기 전용일 때 사용한다. true
updatable 엔티티 수정 시 이 필드도 같이 수정한다. false 옵션은 읽기 전용일 때 사용한다. true
columnDefinition 이터베이스 컬럼 정보를 직접 줄 수 있다.  
table 하나의 엔티티를 두 개 이상의 테이블에 매핑할 때 사용한다. 지정한 필드를 다른 테이블에 매핑할 수 있다. 현재 클래스가 매핑된 테이블
  • @ManyToOne
속성 기능 기본값
targetEntity 연관된 엔티티의 타입정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.  
cascade 영속성 전이 기능을 사용한다.  
fetch 글로벌 페치 전략을 설정한다. @ManyToOne = FetchType.EAGER @OneToMany = FetchType.LAZY
optional false로 설정하면 연관된 엔티티가 항상 있어야 한다. 연관된 객체 즉시로딩시 false 일 경우 Inner Join 을 수행하고 true 일 경우 Outer Join 을 수행한다. https://geco.tistory.com/102 true

5.2 연관관계 사용

5.2.1 저장

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {

            Team team1 = new Team();
            team1.setName("team1");
            em.persist(team1);

            Member member1 = new Member();
            member1.setUsername("member1");
            member1.setTeam(team1); // 연관관계 설정 member1 -> team1
            em.persist(member1);

            Member member2 = new Member();
            member2.setUsername("member2");
            member2.setTeam(team1); // 연관관계 설정 member2 -> team2
            em.persist(member2);

            tx.commit();
        }
        catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }
        finally {
            em.close();
        }

        emf.close();
    }
}
insert into Team (name, team_id) values(?, ?)
insert into Member (team_id, username, member_id) values (?, ?, ?)
insert into Member (team_id, username, member_id) values (?, ?, ?)

5.2.2 조회

  • 객체 그래프 탐색
public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {

            Team team1 = new Team();
            team1.setName("team1");
            em.persist(team1);

            Member member1 = new Member();
            member1.setUsername("member1");
            member1.setTeam(team1); // 연관관계 설정 member1 -> team1
            em.persist(member1);

            Member member2 = new Member();
            member2.setUsername("member2");
            member2.setTeam(team1); // 연관관계 설정 member2 -> team2
            em.persist(member2);

            em.flush();
            em.clear();

            Member findMember = em.find(Member.class, member1.getId());
            Team findTeam = findMember.getTeam(); // 객체 그래프 탐색
            System.out.println("findTeam.getName() = " + findTeam.getName());

            tx.commit();
        }
        catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }
        finally {
            em.close();
        }

        emf.close();
    }
}
  • 객체지향 쿼리 사용(JPQL)
public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {

            Team team1 = new Team();
            team1.setName("team1");
            em.persist(team1);

            Member member1 = new Member();
            member1.setUsername("member1");
            member1.setTeam(team1); // 연관관계 설정 member1 -> team1
            em.persist(member1);

            Member member2 = new Member();
            member2.setUsername("member2");
            member2.setTeam(team1); // 연관관계 설정 member2 -> team2
            em.persist(member2);

            em.flush();
            em.clear();

            String jpql = "select m from Member m join m.team t where t.name = :teamName";

            List<Member> members = em.createQuery(jpql, Member.class)
                    .setParameter("teamName", "team1")
                    .getResultList();

            for (Member member : members) {
                System.out.println("member.getUsername() = " + member.getUsername());
            }
            
            tx.commit();
        }
        catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }
        finally {
            em.close();
        }

        emf.close();
    }
}

5.2.3 수정

  • 트랜잭션 커밋할 때 플러시가 일어나면서 변경감지 기능이 동작한다.
  • Member 의 Team 참조 변수 값만 변경하면 변경감지 기능에 의해 처리된다.
public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {

            Team team1 = new Team();
            team1.setName("team1");
            em.persist(team1);

            Team team2 = new Team();
            team2.setName("team2");
            em.persist(team2);

            Member member1 = new Member();
            member1.setUsername("member1");
            member1.setTeam(team1); // 연관관계 설정 member1 -> team1
            em.persist(member1);

            Member member2 = new Member();
            member2.setUsername("member2");
            member2.setTeam(team1); // 연관관계 설정 member2 -> team2
            em.persist(member2);

            em.flush();
            em.clear();

            Member findMember = em.find(Member.class, member1.getId());
            findMember.setTeam(team2); // team 수정
            
            tx.commit();
        }
        catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }
        finally {
            em.close();
        }

        emf.close();
    }
}
update Member set team_id=?, username=? where member_id=?

5.2.4 연관관계 제거

  • Member 의 Team 참조 변수 값을 null으로 설정하면 연관관계가 제거된다.
Member findMember = em.find(Member.class, member1.getId());
findMember.setTeam(null);

5.2.5 연관된 엔티티 삭제

  • 연관된 엔티티 삭제시 연관관계를 먼저 제거하고 삭제해야한다.
  • 외래 키 제약조건 때문이다.
public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {

            Team team1 = new Team();
            team1.setName("team1");
            em.persist(team1);

            Team team2 = new Team();
            team2.setName("team2");
            em.persist(team2);

            Member member1 = new Member();
            member1.setUsername("member1");
            member1.setTeam(team1); // 연관관계 설정 member1 -> team1
            em.persist(member1);

            Member member2 = new Member();
            member2.setUsername("member2");
            member2.setTeam(team1); // 연관관계 설정 member2 -> team2
            em.persist(member2);

            em.flush();
            em.clear();

            Team findTeam = em.find(Team.class, team1.getId());
            Member findMember1 = em.find(Member.class, member1.getId());
            Member findMember2 = em.find(Member.class, member2.getId());
            findMember1.setTeam(null); // member1 연관관계 제거
            findMember2.setTeam(null); // member2 연관관계 제거
            em.remove(findTeam); // team1 제거

            tx.commit();
        }
        catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        }
        finally {
            em.close();
        }

        emf.close();
    }
}

5.3 양방향 연관관계

  • 회원과 팀의 관계는 다대일(N:1) 단방향 관계이다.
  • 팀과 회원은 일대다(1:N) 단방향 관계이다.
  • 객체 연관관계
    • 회원 객체는 Member.team 필드로 연관관계를 가진다.
    • 팀 객체는 Team.members 필드로 연관관계를 가진다. 일대다(1:N) 이므로 컬렉션을 사용한다.
  • 테이블 연관관계
    • 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 가진다.
    • 회원 테이블과 팀 테이블은 양방향 관계이다.
    • TEAM_ID 외래 키를 통해 회원과 팀을 조인할 수 있고 반대로 팀과 회원을 조인할 수 있다.
    • 단방향 연관관계의 테이블과 차이가 없다.

5.3.1 양방향 연관관계 매핑

  • 팀 엔티티에 List 컬렉션을 추가한다.
  • 일대다 관계를 @OneToMany 으로 매핑한다.
  • mappedBy 속성은 양방향 매핑일 때 반대쪽 매핑의 필드 이름을 값으로 설정한다.(여기서는 Member 엔티티의 team 필드로 설정한다.)
@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String username;

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

    // getter, setter 생략
}
@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name = "team_id")
    private Long id;

    private String name;

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

    // getter, setter 생략
}

5.4 연관관계의 주인

  • 객체의 양방향 연관관계는 정확히 말하면 서로 다른 2개의 단방향 연관관계로 구성되어있다.
  • 데이터베이스의 테이블은 외래 키 하나로 서로 조인할 수 있다. 따라서 테이블은 외래 키 하나만으로 양방향 연관관계를 맺는다.
  • 단방향 매핑시 객체의 참조는 하나이므로 이 참조로 외래 키를 관리하면된다.
  • 양방향 매핑시 객체의 참조는 두개이므로 어떤 참조를 사용해서 외래 키를 관리할지 정해야한다.
  • 두 객체의 연관관계 중 하나를 연관관계의 주인으로 설정하여 외래 키를 관리하도록 해야한다.

5.4.1 양방향 매핑의 규칙: 연관관계의 주인

  • 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고, 외래 키를 관리(등록, 수정, 삭제) 할 수 있다.
  • 주인이 아닌쪽은 읽기만 할 수 있다.
  • 연관관계의 주인이 아닌쪽은 mappedBy 속성을 사용하여 속성의 값으로 연관관계의 주인을 지정한다.
  • MEMBER.TEAM_ID 외래 키를 관리할 관계를 정해야 하는데, Member.team 또는 Team.members 중 하나의 관계를 연관관계의 주인으로 설정해야한다.

5.4.2 연관관계의 주인은 외래 키가 있는 곳

  • 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.
  • 회원 테이블이 외래 키를 가지고 있으므로 Member.team이 주인이 된다.
  • 주인이 아닌 Team.members에는 mappedBy = “team” 속성을 사용하여 주인이 아님을 설정한다.
  • mappedBy 속성의 값은 연관관계의 주인 필드 이름인 “team” 이다.

5.5 양방향 연관관계 저장

  • 양방향 연관관계는 연관관계의 주인이 외래 키를 관리한다.
  • Member 와 Team 의 관계에서 연관관계의 주인은 Member.team 이므로 엔티티 매니저는 Member.team 의 입력 값을 사용하여 외래 키를 관리한다.
Team team1 = new Team();
em.persist(team1);

Member member1 = new Member();
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
em.persist(member1);

Member member2 = new Member();
member2.setTeam(team1); // 연관관계 설정 member2 -> team2
em.persist(member2);

tx.commit();
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
member2.setTeam(team1); // 연관관계 설정 member2 -> team2
team1.getMembers().add(member1) // 연관관계의 주인이 아니므로 무시됨
team1.getMembers().add(member2) // 연관관계의 주인이 아니므로 무시됨

5.6 양방향 연관관계의 주의점

  • 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하면 외래 키가 저장되지 않는다.
Member member1 = new Member();
member1.setUsername("member1");
em.persist(member1);

Member member2 = new Member();
member2.setUsername("member2");
em.persist(member2);

Team team1 = new Team();
team1.setName("team1");
em.persist(team1);

team1.getMembers().add(member1); // 연관관계의 주인이 아닌 곳에만 연관관계 설정
team1.getMembers().add(member2); // 연관관계의 주인이 아닌 곳에만 연관관계 설정

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

  • 연관관계의 주인에만 값을 입력하면 외래 키는 문제없이 저장된다.
  • 그러나 연관관계의 주인이 아닌 곳에도 모두 값을 입력해주는 것이 좋다. 왜냐하면 객체 입장에서는 양쪽에 모두 저장되는 것이 맞기 때문이다.
  • 예를 들면, JPA를 사용하지 않고 엔티티에 대한 테스트를 할 때 문제가 될 수 있다.
Team team1 = new Team();
team1.setName("team1");
em.persist(team1);

Member member1 = new Member();
member1.setUsername("member1");
member1.setTeam(team1);          // 연관관계 설정(주인) member1 -> team1
team1.getMembers().add(member1); // 연관관계 설정 team1 -> member1
em.persist(member1);

Member member2 = new Member();
member2.setUsername("member2");
member2.setTeam(team1);          // 연관관계 설정(주인) member2 -> team1
team1.getMembers().add(member2); // 연관관계 설정 team1 -> member2
em.persist(member2);

5.6.2 연관관계 편의 메소드

  • 양방향 연관관계 설정시 양쪽의 메소드 호출 중 실수로 누락될 수 있으므로 하나의 메소드로 설정하도록 연관관계 편의 메소드를 만드는 것이 좋다.
@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;

    private String username;

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

    // 연관관계 편의 메소드
    public void setTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

5.6.3 연관관계 편의 메소드 작성 시 주의사항

  • 연관관계 편의 메소드 호출시 기존 연관관계를 제거하는 코드가 필요하다.
member1.setTeam(team1); // 1번
member1.setTeam(team2); // 2번
  • 1번 코드 수행시 객체 연관관계

  • 2번 코드 수행시 객체 연관관계

public class Member {
    // ...

    public void changeTeam(Team team) {
        // 기존 팀 관계 제거
        if (this.team != null) {
            this.team.getMembers().remove(this);
        }
        this.team = team;
        team.getMembers().add(this);
    }
}
public class Team {
    // ...

    public void addMember(Member member) {
        if (member.getTeam() != null) {
            member.getTeam().getMembers().remove(member);
        }
        member.setTeam(this); // setter
        members.add(member);
    }
}

Tags:

Categories:

Updated:

Leave a comment