구리

[JPA] 연관관계 매핑 본문

JPA

[JPA] 연관관계 매핑

guriguriguri 2021. 9. 1. 23:49

[예제 시나리오]

- 회원과 팀이 존재한다.

- 회원은 하나의 팀에만 소속될 수 있다.

- 회원과 팀은 다대일 관계이다.

 

 

[단방향 연관관계]

* 객체 지향 모델링 ( 객체 연관관계 사용 )

* Team 엔티티

package hellojpa;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Team {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

* Member 엔티티 수정

package hellojpa;

import javax.persistence.*;
@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "USERNAME")
    private String name;

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

    public Member() {
    }
    
    getter,setter...
}

@ManyToOne 으로 다대일 연관관계를 나타내었고 @JoinColumn으로 엔티티 레퍼런스와 DB에서의 외래키를 매핑시켜주었습니다.

따라서 연관관계, 매핑도 명확하게 했기에 단방향 연관관계의 완벽한 모델링을 나타내었습니다.

* 실습

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
// EntityManagerFactory : persistence.xml 파일에서 persistence-unit에서 설정한 이름으로 애플리케이션에서 1개만 생성하여 공유하여 사용
// EntityManager : DB 작업을 할 때마다 생성해서 사용 (쓰레드간 공유 X, 사용 후 버리기)
// JPA 모든 데이터 변경은 트랜잭션 안에서 실행
// 트랜잭션 설정을 따로 하지 않아도 RDB는 트랜잭션 안에서만 데이터 변경이 가능하게끔 설계
// 따라서 트랜잭션을 사용자가 걸지 않아도 DB가 알아서 처리함
// 트랜잭션 : insert,update 등 DB의 데이터가 변화가 있는 것

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 team = new Team();
            team.setName("TeamA");
            em.persist(team);

            // 회원 저장
            Member member = new Member();
            member.setName("member1");
            member.setTeam(team);   // 단방향 연관관계 설장, 참조 저장
            em.persist(member);

            // 현재 member, team 객체는 1차캐시에 저장되어 있기에
            // 조회해도 select 쿼리문이 실행되지 않음 (DB에서 데이터를 꺼내오지 않음)
            // DB에 select 쿼리문 날리는 걸 보고 싶다면 flush해서 영속성 컨텍스트에 있는 것들 DB에 전달
            // 그 후 1차 캐시 초기화 시켜서 싱크를 맞추자
//            em.flush();
//            em.clear();

            // 조회
            Member findMember = em.find(Member.class, member.getId());

            // 참조를 사용하여 연관관계 조회
            Team findTeam = findMember.getTeam();
            System.out.println("findTeam = " + findTeam.getName());

            // 새로운 팀B
            Team teamB = new Team();
            teamB.setName("TeamB");
            em.persist(teamB);

            // 회원 1에 새로운 팀B 수정
            member.setTeam(teamB);
            System.out.println("member1의 team : " + member.getTeam().getName());
            tx.commit();
        }catch (Exception e){
            tx.rollback();  // 문제 생길 경우 rollback
            e.printStackTrace();
        }finally {
            em.close();     // 사용 후 항상 자원해제
            emf.close();
        }
    }
}
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    call next value for hibernate_sequence
findTeam = TeamA
Hibernate: 
    call next value for hibernate_sequence
member1의 team : TeamB
Hibernate: 
    /* insert hellojpa.Team
        */ insert 
        into
            Team
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (age, USERNAME, TEAM_ID, id) 
        values
            (?, ?, ?, ?)
Hibernate: 
    /* insert hellojpa.Team
        */ insert 
        into
            Team
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* update
        hellojpa.Member */ update
            Member 
        set
            age=?,
            USERNAME=?,
            TEAM_ID=? 
        where
            id=?

 

위 코드를 보면 team 엔티티 생성 후 em.persist(team); 호출하였을 때 team 객체에서 id(PK)가 생성된 후 영속상태로 되었습니다.

그 후 member 엔티티 생성하고 em.persist(member);를 호출하였습니다.

그 다음에 저장한 member 객체를 조회하게 되는데 이때 select 쿼리문이 실행하지 않는 이유는 1차 캐시에 저장되었기에 조회해도 DB에서 꺼내오지 않습니다. 따라서 DB에서 select 쿼리문을 보고 싶다면 flush를 강제 호출하여 영속성 컨텍스트에 있는 것들을 DB에 저장 후 clear하여 1차 캐시 초기화 과정을 진행하여 싱크를 맞추면 됩니다.

 

또한 생성되어있는 member 엔티티의 팀을 TeamB로 변경하고 따로 DB에 저장하지 않았지만 변경사항이 반영된 것을 볼 수 있습니다.

(update 쿼리문 실행되었음)

이는 Dirty Checking이 일어났기에 트랜잭션  커밋시점에 update 쿼리문이 DB에게 전달되었습니다. 

 

 

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

위에서 단방향 관계였던 Member, Team을 양방향으로 변경해보겠습니다.

테이블 상에선 변화가 없고 Team 엔티티에 새로운 멤버 변수를 추가합니다. (Member 엔티티는 동일)

 

 

package hellojpa;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
public class Team {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

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

    getter, setter ...
}

그림을 보면 테이블에선 외래키로 양방향이 가능합니다. (사실상 테이블 연관관계에서 방향이라는 개념 자체가 없지만 편의상)

테이블은 외래키 하나로 두 테이블을 조인할 수 있기 때문입니다. (Member 테이블의 team_id라는 외래키로 테이블을 서로 왔다갔다 할 수 있음)

하지만 객체에는 참조라는 개념이 있기에 앙뱡향으로 설정하려면 서로 다른 단방향 연관관계 2개가 있어야 합니다.

따라서 Team 엔티티에 members라는 참조값을 설정하였고 Team의 입장에선 일대다 관계이기에 @OneTomany(mappedBy = "team")으로 Member의 team에 의해 매핑되었다고 표현하였습니다.

 

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

 

 

* 실습

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;

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 team = new Team();
            team.setName("TeamA");
            em.persist(team);

            // 회원 저장
            Member member = new Member();
            member.setName("member1");
            member.setTeam(team);  
            em.persist(member);

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

            // 조회
            Member findMember = em.find(Member.class, member.getId());

			// member의 team <=> Team의 members 조회 가능
            List<Member> members =  findMember.getTeam().getMembers();

            for (Member m : members) {
                System.out.println("m = " + m.getName());
            }
            tx.commit();
        }catch (Exception e){
            tx.rollback();  // 문제 생길 경우 rollback
            e.printStackTrace();
        }finally {
            em.close();     // 사용 후 항상 자원해제
            emf.close();
        }
    }
}

 

그런데 문제는 연관관계의 주인이 누구인지 지정하지 않는다면 Member의 team이 업데이트 되었을 때 DB의 Member 테이블의 TEAM_ID를 바꿔야할지 Team의 members가 업데이트 됐을 때 DB에 반영해야 할지 혼란스러워집니다.

따라서 둘 중 (Member, Team 객체) 하나로 외래 키를 관리해야 합니다.

연관관계의 주인을 정해야 Member 테이블의 외래키를 관리할 수 있습니다

 

* 연관관계의 주인

객체의 두 관계 중 하나를 연관관계의 주인으로 지정해줘야 하는데 규칙은 다음과 같습니다.

- 연관관계의 주인만이 외래 키를 관리 (등록, 수정)

- 주인이 아닌 쪽은 읽기만 가능

- 주인은 mappedBy 속성 사용 X

- 주인이 아니면 mappedBy 속성으로 주인 지정

( 애초에 ~~에 의해 매핑되었다는 종속적인 의미기에 주인에게 사용하지 않습니다.)

 

 

* 연관관계의 주인을 정하는 법?

- 외래 키가 있는 곳을 주인으로 정하기

- 비즈니스 로직을 기준으로 연관관계의 주인을 선택하지 않기 !! 

 

따라서 Member.team이 연관관계의 주인이 됩니다.

그리고 Team.members는 양방향 관계를 위해 존재할 뿐 가짜 매핑인 셈입니다. (주인의 반대편)

 

 

* 양방향 매핑 ? 단방향 매핑 ? 무엇이 좋을까

처음 테이블을 설계 후 코드를 짤 때는 단방향 매핑만으로도 이미 연관관계 매핑은 완료됩니다.

양방향 매핑은 반대 방향으로 조회 (객체 그래프 탐색) 기능이 추가된 것 뿐이기에 단방향 매핑으로 코드를 짜고 필요하면 양뱡향 매핑 코드를 추가하는 것이 좋습니다.

양방향 매핑으로 변경해도 사실상 테이블에 영향은 가지 않고 주인이 아닌 엔티티에 코드를 추가하는 작업만 하기에 단방향 매핑을 선순위로 잘 짜는 것이 좋습니다.

 


 

[실전 - 연관관계 매핑]

* 단방향

테이블 구조는 이전에 했던 JPA 기초에 있는 실전 테이블과 동일합니다.

양방향으로 매핑하기 전 일단 단뱡향 매핑부터 진행합니다.

package jpabook.jpashop.domain;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "ORDERS")
public class Order {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    private LocalDateTime orderDate;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

   getter, setter ...
}

ORDERS 테이블에 MEMBER_ID라는 외래키가 존재하므로 Order 엔티티에 Member member라는 컬럼을 추가 후 다대일 관계이므로 @ManyToOne, @JoinColumn(name = "MEMBER_ID") 으로 조인할 컬럼을 명시합니다.

 

package jpabook.jpashop.domain;

import javax.persistence.*;

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

    @ManyToOne
    @JoinColumn(name = "ORDER_ID")
    private Order order;

    @ManyToOne
    @JoinColumn(name = "ITEM_ID")
    private Item item;

    private int orderPrice;

    private int count;

    getter, setter...
}

ORDER_ITEM 테이블에는 2개의 외래키가 존재하므로 해당 엔티티에 2개의 참조값을 추가합니다.

마찬가지로 다대일 관계이므로 @ManyToOne, @JoinColumn 어노테이션을 사용합니다.

 

 

* 양방향

(양방향 매핑이 필요하다면 코드를 추가로 작성하면 됩니다. 하지만 Member에서 orders는 사실상 크게 쓰일 이유가 없기에 실무에서는 회원 엔티티에 주문 관련 멤버변수를 추가할 일이 적습니다...)

package jpabook.jpashop.domain;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    private String name;
    private  String city;
    private String street;
    private String zipcode;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<Order>();

    getter, setter ...
}

Member, Order 관계에서 외래키인 MEMBER_ID는 ORDERS 테이블에 존재하므로 주인은 Order 엔티티가 됩니다.

따라서 Member 엔티티에 orders 라는 참조값을 설정하고 mappedBy를 이용해 연관관계의 주인을 설정합니다.

 

package jpabook.jpashop.domain;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "ORDERS")
public class Order {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<OrderItem>();

    private LocalDateTime orderDate;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

	getter, setter ...
}

ORDER_ID라는 외래키도 ORDER_ITEM이라는 테이블이 가지고 있으므로 OrderItem과 Order의 연관관계 주인도 OrderItem이 됩니다. 따라서 Order 엔티티에 List<OrderItem> orderItems 변수를 추가하고 연관관계의 주인을 설정합니다.

 

출처 : 자바 ORM 표준 JPA 프로그래밍 - 김영한

'JPA' 카테고리의 다른 글

[JPA] 다양한 연관관계 매핑  (0) 2021.09.02
[JPA] JPA 기초 & 영속성 컨텍스트  (0) 2021.08.30