WEB/JPA

김영한 (ORM 표준 JPA 프로그래밍 5) 연관관계 매핑 기초

Tony Lim 2021. 3. 7. 23:32
728x90

 

package hellojpa;

import javax.persistence.*;

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

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

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


    public Long getId()
    {
        return id;
    }

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


    public Long getTeamId()
    {
        return teamId;
    }

    public void setTeamId(Long teamId)
    {
        this.teamId = teamId;
    }

    public String getUsername()
    {
        return username;
    }

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

}
package hellojpa;

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

@Entity
public class Team
{
    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    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;
    }
}
package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain
{
    public static void main(String[] args)
    {
        // emf 는 딱 하나만 만들어야한다.
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        // 트랜잭션 단위를 실행할떄마다 생성해주어야한다. dbconnection 을 얻어서 쿼리를 날리고 종료하는
        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.setUsername("member1");
            member.setTeamId(team.getId());
            em.persist(member);

            Member findMember = em.find(Member.class, member.getId());
            Long findTeamId = findMember.getTeamId();
            Team findTeam = em.find(Team.class, findTeamId);



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

em persist 한 순간 db로 부터 id를 가져올 수있으므로 team.getId()가 가능하다.

테이블은 왜래키로 조인을 사용해서 연관된 테이블을 찾는다. 참조 대신에 왜래 키를 그대로 사용해서 객체를 모델링 한 결과이다.

객체간의 참조가 매우 불편하다. 객체지향스럽지 못하다. 

 

단방향 연관관계

//    @Column(name = "TEAM_ID")
//    private Long teamId;

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

Member가 많이 있고 Team은 하나다. 멤버 클래스 입장에서는 @many to one 이다.

또한 이 Team team 이 어떠한 column이랑 매핑이 될것이냐는 @JoinColumn으로 db에 있는 TEAM_ID column과 매핑이될것이다를 표시해준다.

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

            Member member = new Member();
            member.setUsername("member1");
            member.setTeam(team);
            em.persist(member);

            Member findMember = em.find(Member.class, member.getId());
            Team findTeam = findMember.getTeam();



            tx.commit();
        }

그러면 더이상 이제 왜래키를 통해 db 처럼 참조할 필요없이 바로 객체를 참조가 가능하다.

 

 

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

package hellojpa;

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

@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<>();

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

    public List<Member> getMembers()
    {
        return members;
    }

    public void setMembers(List<Member> members)
    {
        this.members = members;
    }
}

현재 Team 클래스임으로 One to many 매핑을 해준다. 하지만 여기는 Member의 many to one과 다르게 mappedby 가 붙는다. 왜 그럴까?

사실상 단방향 2개를 양방향으로 우기고 있는 샘이다. 테이블의 경우에는 Foreign Key 값하나로 참조 가 끝난다.

select * from member m
join team t on m.team_id = t.team_id

select * from team t
join member m on t.team_id = m.team_id

하지만 객체의 경우 참조가 양쪽에 존재한다. 둘 중에 하나가 외래키랑 매핑이되어 담당해주어야한다.

항상 외래 키가 있는 클래스 (여기에선 Member가 그러하다) 를 주인으로 삼아야 한다.

mappedby는 주인이 아닌 다른 객체에 달아주어야한다. 거기에서는 읽기만 가능하다. 써도 DB에 반영이 되지않는다.


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

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)
    {
        // emf 는 딱 하나만 만들어야한다.
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        // 트랜잭션 단위를 실행할떄마다 생성해주어야한다. dbconnection 을 얻어서 쿼리를 날리고 종료하는
        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.setUsername("member1");
            member.setTeam(team);
            em.persist(member);

//            team.getMembers().add(member);

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

            Team findTeam = em.find(Team.class, team.getId());
            List<Member> members = findTeam.getMembers();

            for (Member m : members)
            {
                System.out.println("m = " + m.getUsername());
            }


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

이 상태에서 현재 Team 객체가 영속성 상태로 1차 캐시에 저장이 되어있는데 List<Member>는 텅 비어있는 상태이다. 그래서 for 문이 출력이 안될텐데 이는 객체지향적이지 못하다. 

또한 테스트를 작성할 때도 문제가 된다. 주인쪽에만 값을 쓰는것이 맞지만 순수 객체 상태를 고려해서 항상 양쪽에 값을 써주는 것이 맞다. 

하지만 매번 team.getmembers().add(member) 를 써주는것을 어색해서 까먹을 수 도 있다. 따라서 연관관계 편의 메소드를 생성하는것이 좋다.

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

이렇게 하면 setTeam을 하는 시점에 알아서 세팅이 되기 때문이다. 양쪽으로 값이 탁 걸리는것이다.

김영한님의 경우에는 관례처럼 setTeam으로 메소드명을 정하지않고 changeTeam 처럼 다른 이름을 지정해서 중요한 뭔가 일을 하는 거구나를 알게끔 해준다.

Entity 는 Controller에서 DTO로 변환을 해주어서 반환해야 문제가 안생긴다. entity가 변환이되면 그걸 반환하는 api의 스펙이 바뀐셈이된다. 항상 고정적인 dto를 마련해주어 이런 번거로움이 없도록 하자

또한 설계할 떄는 단방향 매핑을 해놓고 실제 개발할 때 필요하면 양방향 매핑을 하는것이 편리하다.

 

실전예제 2 - 연관관계 매핑 시작

package jpabook.jpashop.domain;

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

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

//    @Column(name="MEMBER_ID")
//    private Long memberId;

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

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

    public void addOrderItem(OrderItem orderItem)
    {
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }

    private LocalDateTime orderDate;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    public Long getId()
    {
        return id;
    }

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

    public Member getMember()
    {
        return member;
    }

    public void setMember(Member member)
    {
        this.member = member;
    }

    public LocalDateTime getOrderDate()
    {
        return orderDate;
    }

    public void setOrderDate(LocalDateTime orderDate)
    {
        this.orderDate = orderDate;
    }

    public OrderStatus getStatus()
    {
        return status;
    }

    public void setStatus(OrderStatus status)
    {
        this.status = status;
    }

}

Foreign Key가 있는쪽이 주인이 되어야함으로 @JoinColumn을 넣어준다.

mappedby 는 OrderItem을 바로 조회하고싶어서 양방향 기능을 설정해준것이다. 또한 객체지향적으로 코드를 작성하기위해 addOrderItem에서 연결을 해준다.

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<>();
    
    public void addOrder(Order order)
    {
        orders.add(order);
        order.setMember(this);
    }
    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;
    }

    public String getCity()
    {
        return city;
    }

    public void setCity(String city)
    {
        this.city = city;
    }

    public String getStreet()
    {
        return street;
    }

    public void setStreet(String street)
    {
        this.street = street;
    }

    public String getZipcode()
    {
        return zipcode;
    }

    public void setZipcode(String zipcode)
    {
        this.zipcode = zipcode;
    }
}

양방향 매핑을 위한 mappedby를 해주었다. 역시 이 경우에도 addOrder를 통해 양쪽연결을 해주었다. 

package jpabook.jpashop.domain;

import javax.persistence.*;

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

//    @Column(name = "ORDER_ID")
//    private Long orderId;

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

//    @Column(name = "ITEM_ID")
//    private Long itemId;

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


    private int orderPrice;
    private int count;

    public Long getId()
    {
        return id;
    }

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

    public Order getOrder()
    {
        return order;
    }

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

    public Item getItem()
    {
        return item;
    }

    public void setItem(Item item)
    {
        this.item = item;
    }

    public int getOrderPrice()
    {
        return orderPrice;
    }

    public void setOrderPrice(int orderPrice)
    {
        this.orderPrice = orderPrice;
    }

    public int getCount()
    {
        return count;
    }

    public void setCount(int count)
    {
        this.count = count;
    }
}

2개의 Foreign Key를 가졌음으로 둘다 단방향 매핑을 해주고 주인임으로 @JoinColumn을 해준다.

728x90