자바 ORM 표준 JPA 프로그래밍

[자바 ORM 표준 JPA 프로그래밍] 5장 - 연관관계 매핑 기초

jny0 2023. 5. 1. 18:16

 

 

자바 ORM 표준 JPA 프로그래밍 | 김영한 - 교보문고

자바 ORM 표준 JPA 프로그래밍 | 자바 ORM 표준 JPA는 SQL 작성 없이 객체를 데이터베이스에 직접 저장할 수 있게 도와주고, 객체와 관계형 데이터베이스의 차이도 중간에서 해결해준다. 이 책은 JPA

product.kyobobook.co.kr

 

5장 - 연관관계 매핑 기초

 

  • 방향 : 단방향과 양방향
  • 다중성 : 일대일, 일대다, 다대일, 다대다
  • 연관관계의 주인 : 양방향 연관관계 시 연관관계의 주인을 정해야 함

 

1. 단방향 연관관계

객체 연관관계와 테이블 연관관계의 차이

  • 객체는 참조(주소)로 연관관계를 맺는다
    • 객체의 연관관계는 단방향이다.
    • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
  • 테이블의 외래 키로 연관관계를 맺는다.
    • 테이블의 연관관계는 양방향이다.

 

1.1 순수한 객체 연관관계

  • 객체는 참조를 사용해서 연관관계 탐색 - 객체 그래프 탐색
public class Member {

    private String id;
    private String username;

    private Team team; // 팀의 참조를 보관

        public void setTeam(Team team) {
            this.team = team;
        }

    // Getter, Setter
}

public class Team {

    private String id;
    private String name;

    // Getter, Setter
}

public static void main(String[]args){
    Member member1 = new Member("member1","회원1");
    Member member2 = new Member("member2","회원2");
    Team team1 = new Team("team1","팀1");

    member1.setTeam(team1);
    member2.setTeam(team1);

    Team findTeam = member1.getTeam(); //팀1 조회
}

 

1.2 테이블 연관관계

  • 데이터베이스는 외래 키를 사용해서 연관관계 탐색 - 조인
SELECT T.*
FROM MEMBER M
    JOIN TEAM T ON M.TEAM_ID = T.ID 
WHERE M.MEMBER_ID = 'member1' // member1이 속한 팀 조회

 

1.2 객체 관계 매핑

회원 객체의 Member.team 필드와 회원 테이블의 MEMBER.TEAM_ID 외래 키 컬럼를 매핑

@Entity
public class Member {

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

    // 연관 관계 매핑
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    // Getter, Setter
}

@Entity
public class Team {

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

    // Getter, Setter
}

 

@ManyToOne

  • 다대일(N:1) 관계라는 매핑 정보 / 연관관계 매핑 시 다중성을 나타내는 어노테이션은 필수
    • optional : false로 설정하면 연관된 엔티티가 항상 있어야 함
    • fetch : 글로벌 페치 전략 설정
    • cascade : 영속성 전이 기능 사용
    • targetEntity : 연관된 엔티티의 타입 정보를 설정

 

@JoinColumn

  • 외래 키를 매핑할 때 사용 / 생략 가능
    • name : 매핑할 외래 키 이름 지정
    • referencedColumnName : 외래키가 참조하는 대상 테이블의 컬럼명
    • foreignKey : 외래 키 제약조건 직접 설정 (테이블 생성 시에만 사용)
    • 이 외에 @Column 의 속성도 사용 가능
  • 생략 시 기본 전략 사용 : 필드명 + _ + 참조하는 테이블의 컬럼명 (ex. team_TEAM_ID)

 

2. 연관관계 사용

2.1 저장

  • JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
public void testSave(){
    Team team1 = new Team("team1","팀1");
    em.persist(team1);

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

    Member member2 = new member("member2","회원2");
    member2.setTeam(team1);
    em.persist(member2);
}
  • 회원 엔티티가 팀 엔티티를 참조하고 저장하고, JPA는 참조한 팀의 식별자(Team.id)를 외래 키로 사용해서 적절한 등록 쿼리를 생성한다.

 

2.2 조회

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

 

객체 그래프 탐색

Member member = em.find(Member.class, "member1");
Team team = member.getTeam();

 

객체지향 쿼리 사용(JPQL)

  • 팀 1에 소속된 회원들만 조회
  • JPQL에서 : 로 시작하는 것은 파라미터를 바인딩받는 문법
String jpql = "select m from Member m join m.team where" + "t.name=:teamName";

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

 

2.3 수정

  • 단순히 불러온 엔티티의 값만 변경 → 변경 감지 기능으로 DB에 자동 반영
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); // 수정
}

 

2.4 연관관계 제거

  • 연관관계를 null로 설정
private static void deleteRelation(EntityManager em){
    Member member1 = em.find(Member.class,"member1");
    member1.setTeam(null);
}

 

2.5 연관된 엔티티 삭제

  • 연관관계를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다.
    • 그렇지 않으면 외래 키 제약 조건으로 인해 DB에서 오류가 발생한다
member1.setTeam(null);
member2.setTeam(null);
em.remove(team);

 

 

3. 양방향 연관관계

  • 객체 연관관계
    • 회원 → 팀 : 다대일 관계
    • 팀 → 회원 : 컬렉션 사용, 일대다 관계
  • 테이블은 외래 키 하나로 양방향 조회가 가능하므로 DB에 추가해야 할 내용은 없다.
@Entity
public class Member {

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

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

    // Getter, Setter
}

@Entity
public class Team {

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

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

    // Getter, Setter
}

 

 

4. 연관관계의 주인

테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다. 엔티티를 양방향으로 매핑하면 회원 → 팀 / 팀 → 회원 두 곳에서 서로를 참조한다. 즉, 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데, 외래키는 하나인 차이가 발생한다.

따라서 두 객체 연관관계 중 하나를 연관관계의 주인으로 정해서 테이블의 외래 키를 관리해야 한다.

 

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

  • 연관관계의 주인 만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있고, 주인이 아닌 쪽은 읽기만 가능하다.
  • 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아니면 mappedBy 속성을 사용해서 연관관계의 주인을 지정해야 한다.

 

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

  • 연관관계의 주인은 테이블에 외래키가 있는 곳으로 정해야 한다.
  • 일대다, 다대일 관계에서는 항상 ‘다’ 쪽이 외래키를 가진다. 그러므로 @ManyToOne은 항상 연관관계의 주인이 된다.

 

 

5. 양방향 연관관계 저장

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

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

    Member member2 = new member("member2","회원2");
    member2.setTeam(team1);
    em.persist(member2);
}
  • 단방향 연관관계 저장 코드와 완전히 같다.
  • 주인이 아닌 곳에 입력된 값은 외래키에 영향을 주지 않는다.
    • team1.getMembers().add(member1) 코드에서 Team.members는 연관관계의 주인이 아니므로 DB에 저장할 때 해당 코드는 무시된다.

 

 

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

  • 가장 흔히 하는 실수는 주인이 아닌 곳에 값을 입력하는 것이다.
    • 이렇게 되면 DB 저장 시 해당 코드는 무시되므로 외래 키의 값도 null이 저장된다.

 

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

  • 사실 객체 관점에서는 양쪽 방향에 모두 값을 입력해 주는 것이 가장 안전하다.
  • 연관관계의 주인에게만 값을 입력하면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.

결론 : 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주자

public void test순수한객체_양방향(){
    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(team2);
    team1.getMembers().add(member2);
    em.persist(member2);
}

 

6.2 연관관계 편의 메소드

양 쪽 모두 관계를 맺는 코드를 하나의 메소드로 묶어서 사용하면 실수할 일이 줄어든다.

public class Member {

    private Team team;

    public void setTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

member1.setTeam(teamA); 를 설정하고 teamA를 teamB로 변경하고 싶어

member1.setTeam(teamB); 를 설정하면 teamA -> member1 관계를 제거되지 않은 상태에서 teamB와 연결되게 된다. 따라서 연관관계를 변경할 때, 기존 팀이 있으면 삭제하는 코드를 추가해야 한다.

public void setTeam(Team team){

      if(this.team != null){
          this.team.getMembers().remove(this);
      } 

      this.team = team;
      team.getMembers().add(this);
}