[자바 ORM 표준 JPA 프로그래밍 - 기본편] 05. 연관관계 매핑 기초

Turtle·2024년 6월 18일
0
post-thumbnail

🙄테이블에 맞추어 설계한 경우(협력 관계를 만들기 어려움)

✔️Member 클래스

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

	@Column(name = "TEAM_ID")
	private Long team_id;

	private String username;

	// Getter/Setter ...
}

✔️Team 클래스

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

	// Getter/Setter ...
}

✔️JpaMain 클래스

public class JpaMain {

    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction transaction = em.getTransaction();
        transaction.begin();

        try {
            Team team = new Team();
            team.setName("teamA");
            em.persist(team);

            Member member = new Member();
            member.setUsername("userA");
            member.setTeam_id(team.getId());
            em.persist(member);

            transaction.commit();
        } catch (Exception e) {
            transaction.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

🙄단방향 연관관계

  • ✔️객체 지향 모델링
    • 연관관계 저장을 위해 참조를 사용하여 연관관계를 조회
    • DB의 관점에서 보았을 때를 기준으로 한다.
    • Team과 Member의 관계를 생각해보면 하나의 Team에 여러 Member가 있을 수 있으므로 Team : Member = 1 : N이다.
    • Team의 기준에서 보았을 때 일대다, Member의 기준에서 보았을 때 다대일이다.

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

    private String name;

	//////////////////////////////// 핵심 코드
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    public Team getTeam() {
        return team;
    }

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

	// Getter/Setter ...
}
  • ✔️코드 핵심
    • 테이블 중심이 아니라 객체 중심으로 설계
    • Member를 기준으로 하기에 다대일 관계로 @ManyToOne 어노테이션 사용
    • 테이블에서 Member 테이블에 있는 TEAM_ID(FK)에 매핑을 하는 과정에서 @JoinColumn 어노테이션을 사용
  • ✔️@JoinColumn
    • @JoinColumn은 외래 키를 매핑할 때 사용한다.
  • ✔️@ManyToOne
    • @ManyToOne은 다대일 관계에서 사용한다.

🙄양방향 연관관계와 연관관계의 주인

  • ✔️객체의 양방향 관계
    • 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
    • 객체를 양방향으로 참조하려면 단방향 연관관계 2개를 만들어야 한다.
      • A → B (a.getB())
      • B → A (b.getA())
class A {
	B b;
}

class B {
	A a;
}
  • ✔️테이블의 양방향 관계
    • 테이블에서는 외래 키 하나로 두 테이블의 연관관계를 관리
    • 객체에서는 양측 참조를 하는 단방향 연관관계 2개를 만들고 테이블에서는 외래 키 하나로 연관관계를 관리하는데 그렇다면 객체에서 어느 키를 외래 키 역할로 설정해야할까?
  • ✔️연관관계의 주인
    • 양방향 매핑 규칙
      • 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
      • 연관관계의 주인만이 외래 키를 관리(등록, 수정)
      • 주인이 아닌 쪽은 읽기만 가능
      • 주인은 mappedBy 속성 사용 불가
      • 주인이 아니면 mappedBy 속성으로 주인을 지정
    • 주인을 지정할 때는 외래 키가 있는 곳을 주인으로 정해라.
    • DB의 N(다)쪽이 연관관계의 주인이 된다.
    • 연관관계의 주인에 값을 입력하지 않고 주인이 아닌 곳에만 값을 입력하면 정상적으로 저장되지 않는다.
public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("ex");
        EntityManager em = emf.createEntityManager();
        EntityTransaction et = em.getTransaction();

        et.begin();

        try {
        
            // [1]. 연관관계의 주인이 아닌 곳에서 값을 입력하는 경우 발생할 수 있는 경우
            Team team = new Team();
            team.setName("TeamA");
            em.persist(team);

            Member memberA = new Member();
            memberA.setName("memberA");
            team.getMembers().add(memberA);
            em.persist(memberA);

            Member memberB = new Member();
            memberB.setName("memberB");
            team.getMembers().add(memberB);
            em.persist(memberB);

            et.commit();
        } catch (Exception e) {
            et.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}
public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("ex");
        EntityManager em = emf.createEntityManager();
        EntityTransaction et = em.getTransaction();

        et.begin();

        try {
            Team team = new Team();
            team.setName("TeamA");
            em.persist(team);

            /*
            // [1]. 연관관계의 주인이 아닌 곳에서 값을 입력하는 경우 발생할 수 있는 경우
            Member memberA = new Member();
            memberA.setName("memberA");
            team.getMembers().add(memberA);
            em.persist(memberA);

            Member memberB = new Member();
            memberB.setName("memberB");
            team.getMembers().add(memberB);
            em.persist(memberB);
            */

            // [2]. 연관관계의 주인에서 값을 입력
            Member memberA = new Member();
            memberA.setName("memberA");
            memberA.setTeam(team);
            em.persist(memberA);

            Member memberB = new Member();
            memberB.setName("memberB");
            memberB.setTeam(team);
            em.persist(memberB);

            et.commit();
        } catch (Exception e) {
            et.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}
  • ✔️정리
    • 위의 메인 코드에서 보면 Team 클래스에 있는 컬렉션을 Getter로 가져와서 add() 메서드를 호출하여 추가하는 것으로 볼 수 있고 정상적으로 동작하는 것처럼 보여지나 결과는 정상적으로 동작하지 않는 것을 볼 수 있다.
    • 왜냐하면 연관관계의 주인은 Member 클래스 안에 있는 참조 필드인 Team이기 때문에 연관관계의 주인이 아닌 Team 클래스에서 Getter로 가져와서 add() 메서드로 호출한다한들 정상적으로 저장이 되지 않는 것이다. 위의 코드를 바로 잡으려면 아래와 같이 수정해야 한다.
    • 양방향 연관관계에서 항상 양쪽에 값을 설정하고 연관관계의 주인이 있는 곳에서 직접적인 설정을 해줘야 정상적인 동작을 할 수 있게 된다.
    • 양방향 매핑시에 무한 루프를 조심하자.
    • 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자. → by 연관관계 편의 메서드

🙄실전 예제 작성 코드 - 연관관계 매핑해보기

  • ✔️단방향으로 할지 양방향으로 할지..(웬만하면 단방향이 좋음)
    • 회원 입장에서 주문 리스트를 관리할 필요가 있다고 판단되면 양방향 매핑
    • 주문 입장에서 이 주문을 한 회원이 누구인지 알 필요가 있음
      • 따라서 회원과 주문에서 연관관계의 주인은 주문이 됨
    • 주문 입장에서 주문상품 리스트를 관리할 필요가 있다.(실제 비즈니스 로직에서도 그럼)
    • 주문상품 입장에서 이 주문상품이 어떤 주문에 속해있는지를 알 필요가 있음
      • 따라서 주문과 주문상품에서 연관관계의 주인은 주문상품이 됨
    • 상품 입장에서 해당 상품이 여러 다른 주문서에서 주문이 될 수 있으므로 주문과 주문상품의 관계는 1:N이고 일반적으로 N(다)쪽에서 외래 키를 관리
      • 따라서 주문상품과 상품에서 연관관계의 주인은 주문상품이 됨

✔️Member 클래스

@Entity
@Table(name = "MEMBERS")
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 클래스

@Entity
@Table(name = "ORDERS")
public class Order {
    @Id @GeneratedValue
    @Column(name = "ORDER_ID")
    private Long id;
    
    /////////////////////////////////////////
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    private LocalDateTime orderDate;
    @Enumerated(EnumType.STRING)
    private OrderStatus status;

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

	// 연관관계 편의 메서드
    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }
}

✔️OrderItem 클래스

@Entity
public class OrderItem {
    @Id @GeneratedValue
    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;

    public void setOrder(Order order) {
        this.order = order;
    }
}

✔️Item 클래스

@Entity
public class Item {
    @Id @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    private String name;
    private int price;
    private int stockQuantity;
}

0개의 댓글